Wikilivres frwikibooks https://fr.wikibooks.org/wiki/Accueil MediaWiki 1.47.0-wmf.22 first-letter Média Spécial Discussion Utilisateur Discussion utilisateur Wikilivres Discussion Wikilivres Fichier Discussion fichier MediaWiki Discussion MediaWiki Modèle Discussion modèle Aide Discussion aide Catégorie Discussion catégorie Transwiki Discussion Transwiki Wikijunior Discussion Wikijunior TimedText TimedText talk Module Discussion module Event Event talk Fonctionnement d'un ordinateur/Le parallélisme mémoire 0 65888 773848 773827 2026-10-01T12:22:49Z Mewtow 31375 /* Le ré-ordonnancement des commandes mémoires */ 773848 wikitext text/x-wiki Dans ce chapitre, nous allons voir les techniques qui permettent de gérer plusieurs accès mémoire simultanés directement au niveau du cache ou de la mémoire RAM. Par plusieurs accès mémoire simultanés, vous pensez sans doute à l'usage de cache multiports, voire de mémoires RAM multiport. C'est en effet une solution, mais ce n'est clairement pas la seule. Par exemple, il est possible de pipeliner l'accès au cache, voire à la mémoire RAM. Il existe de nombreuses techniques de '''parallélisme mémoire''', et nous allons les voir dans ce chapitre. Pour donner un exemple, il est possible de pipeliner les accès mémoire. Il est possible de pipeliner l'accès au cache et/ou l'accès à la mémoire, et nous verrons comment faire dans ce chapitre. Pipeliner l'accès au cache est une technique en vigueur dans les processeurs modernes, qu'ils aient ou non un pipeline dynamique. Par contre, pipeliner la mémoire est une technique ancienne, aujourd'hui peu utilisée. Elle était utilisée sur des très vieux ordinateurs, qui avaient un pipeline mais pas de mémoire cache. Mais de nos jours, les mémoires SDRAM et DDR ne sont pas adaptées pour ça, il est très difficile de les pipeliner correctement. Le parallélisme mémoire est utile aussi bien sur des processeurs à émission dans l'ordre que dans le désordre. Par exemple, un processeur ''in-order'' avec des lectures non-bloquantes peut en profiter. Il peut techniquement lancer une seconde lecture, après avoir rencontré une première lecture non-bloquante. En général, le nombre de lectures consécutives est cependant limité à moins d'une dizaine. À l'opposé, les processeurs sans lectures non-bloquantes profitent pas du parallélisme mémoire. À la rigueur, ils peuvent en profiter s'ils intègrent des techniques de préchargement. Pendant que le processeur exécute une lecture/écriture, il peut précharger une autre donnée en parallèle. Inutile de dire que sans pipeline processeur, le parallélisme mémoire ne sert pas à grand-chose. ==Le ''line fill buffer'' et les techniques associées== Lors d'un défaut de cache, le processeur doit attendre que toute la ligne de cache soit chargée avant d'être utilisable. Or, la taille d'une ligne de cache est supérieure à la largeur du bus mémoire, ce qui fait qu'une ligne de cache est chargée en plusieurs fois, morceaux par morceaux, mot mémoire par mot mémoire. Le chargement peut se faire directement dans le cache, mais ce n'est pas une solution très pratique. À la place, beaucoup de processeurs ajoutent une mémoire tampon entre la RAM et le cache, appelée le '''tampon de remplissage de ligne''' (''line-fill buffer'') dans les processeurs modernes. Lors d'un défaut de cache, le processeur charge la donnée de la RAM dans le tampon de remplissage de ligne, mot mémoire par mot mémoire. Une fois plein, le tampon de remplissage de ligne est recopié dans la ligne de cache. Le tampon de remplissage de ligne contient une ligne de cache, si ce n'est que certains bits de contrôle ne sont pas présents. Le tampon de remplissage de ligne contient un bit de validité pour chaque mot mémoire de la ligne de cache, qui indiquent si le mot mémoire a été chargé. Par exemple, prenons un processeur 64 bits, qui gère donc des mots mémoire de 8 octets, avec des lignes de cache 256 octets/32 mots mémoire. Le tampon de remplissage de ligne contiendra 32 bits de validité. Si le processeur a chargé les 6 premiers mots mémoire, les 6 premiers bits de validité seront mis à 1, les autres seront encore à zéro. Il faut noter que la disponibilité de la ligne complète se détermine assez facilement en faisant un ET logique entre tous les bits de validité. Le processeur sait ainsi quand la ligne de cache est disponible entièrement et donc quand la transférer dans le cache. La même chose existe avec une hiérarchie de cache, sauf que l'on trouve une mémoire tampon entre chaque niveau de cache. Il y a un tampon de remplissage de ligne entre le cache L1 et le cache L2, entre le cache L2 et le L3, etc. Si le cache ne gère qu'un seul défaut de cache à la fois, le ''fill line buffer'' est une mémoire très simple, qui ne mémorise qu'une seule ligne de cache. Et cela vaut aussi bien pour un cache bloquant que non-bloquant. Mais sur les caches capables de gérer plusieurs défauts simultanés, le tampon de remplissage de ligne est une mémoire de type FIFO ou LIFO, capable de mémoriser plusieurs lignes de cache. Notons que le tampon de remplissage de ligne est très utile pour implémenter certaines techniques, par exemple le contournement du cache. Nous avions vu dans le chapitre sur le cache que certains accès mémoire doivent contourner le cache, pour des raisons de cohérence des caches. C'est notamment nécessaire pour accéder aux périphériques, mais c'est aussi utile pour des raisons de performances dans des cas très spécifiques. Les accès qui contournent le cache se font directement dans le tampon de remplissage de ligne : le processeur écrit ou lit les données depuis ce tampon de remplissage de ligne, sans accéder au cache. ===L'''early restart'' et le ''critical word load''=== La présence du ''line buffer'' permet une optimisation assez intéressante, qui permet de réduire la latence des défauts de cache. L'optimisation consiste à lire un mot mémoire dans le tampon de remplissage de ligne, même si la ligne de cache complète n'a pas encore été chargée. Il existe deux manières de faire cela, qui portent les noms d'''early restart'' et de ''critical word load''. La première est la version la plus simple, la seconde est plus complexe mais plus performante. Avec la technique d''''''early restart''''', la ligne de cache est chargée normalement, en partant de son premier mot mémoire. Dès que le mot mémoire lu/écrit par le processeur est copié dans le ''line fill buffer'', il est envoyé au processeur immédiatement. Illustrons le tout par un exemple, où une ligne de cache fait 16 mots mémoire. Le processeur effectue une lecture, qui lit le 5ème mot mémoire. Sans ''early restart'', le processeur doit charger les 16 mots mémoire avant de faire la lecture dans le cache. Avec ''early restart'', le processeur reçoit la donnée dès que le 5ème mot mémoire est disponible. Le processeur doit attendre que les 4 premiers mots mémoire soient chargés, puis le 5ème mot mémoire arrive et est envoyé directement au processeur, il est lu directement depuis le ''line fill buffer''. Les 11 mots mémoire suivants sont ensuite chargés dans le cache pendant que le processeur fait des calculs dans son coin. Le ''critical word load'' est une optimisation de la technique précédente où le chargement de la ligne de cache commence directement à la donnée demandée par le processeur. Pour reprendre l'exemple précédent, où le processeur demande le 5ème mot mémoire sur 16, le ''critical word load'' charge le 5ème mot mémoire en premier et l'envoie au processeur, ce qui fait qu'il est chargé très rapidement. Pas besoin d'attendre que le processeur charge les 4 mots mémoire précédents comme avec l'''early restart''. Dans le détail, le processeur charge le 5ème mot mémoire en premier, puis charge les 11 suivants, et termine par les 4 mots mémoire du début. En clair, le ''critical word load'' commence par charger le mot mémoire lu, puis les blocs suivants, avant de revenir au début du bloc pour charger les blocs restants. Ainsi, la donnée demandée par le processeur sera la première disponible. Pour cela, l'organisation du tampon de remplissage de ligne est modifiée de manière à rendre cela possible. Il a une taille égale à une ligne de cache complète, qui contient elle-même plusieurs mots mémoire. Dans le ''line-fill buffer'', chaque mot mémoire est stocké avec un tag, qui indique l'adresse du mot mémoire stocké dans le ''line-fill buffer''. Le ''line-fill buffer'' est donc un cache un peu particulier, qui fonctionne comme un cache du point de vue du processeur, comme une mémoire FIFO pour les transferts avec le cache. Ainsi, un processeur qui veut lire dans le cache après un défaut peut accéder à la donnée directement depuis le tampon de remplissage de ligne, alors que la ligne de cache n'a pas encore été totalement recopiée en mémoire. ==Les caches pipelinés== Il est possible de pipeliner l'accès au cache, ce qui demande juste de rajouter des registres au bon endroits et quelques circuits de contrôle. Pipeliner l'accès au cache est une technique en vigueur dans les processeurs modernes, même ceux avec un pipeline dynamique. Et l'implémentation peut se faire de plusieurs manières différentes. Il faut dire que découper une mémoire cache en plusieurs étapes peut se faire de plusieurs manière. Commençons par la plus simple. Beaucoup de processeurs des années 2000 avaient une fréquence peu élevée comparée aux standards d'aujourd'hui, ce qui fait que leur cache avait bien un temps d'accès d'un cycle d'horloge. Mais l'accès au cache se faisait en deux cycles d'horloge : un cycle pour calculer une adresse et un cycle pour l'accès mémoire proprement dit. Et ces deux étapes étaient pipelinées, à savoir que deux micro-opérations mémoire peuvent s'exécuter en même temps : une dans l'étage de calcul d'adresse, une autre dans le cache. L'unité mémoire était donc pipelinée, alors que l'accès au cache ne l'était pas. Les processeurs qui implémentaient cette technique regroupent les micro-architectures K5 et K6 d'AMD, les processeurs Intel de micro-architecture P6 (Pentium 2 et 3) et quelques autres. Il est aussi possible de pipeliner l'accès au cache lui-même. Avec un cache pipeliné, l'accès au cache ne se fait pas en un seul cycle, mais en plusieurs. Cependant, on peut lancer un nouvel accès au cache à chaque cycle d'horloge, comme un processeur pipeliné lance une nouvelle instruction à chaque cycle. L'implémentation est assez simple : il suffit d'ajouter des registres dans le cache. Pour cela, on profite que le cache est composé de plusieurs composants séparés, qui échangent des données dans un ordre bien précis, d'un composant à un autre. De plus, le trajet des informations dans un cache est linéaire, ce qui les rend parfois pour l'usage d'un pipeline. ===Le ''pipelining'' des caches ''direct-mapped''=== Voici ce qui se passe avec un cache directement adressé. Pour rappel ce genre de cache est conçu en combinant une mémoire RAM, généralement une SRAM, avec quelques circuits de comparaison et un MUX. L'accès au cache se fait globalement en deux étapes : la première lit la donnée et le ''tag'' dans la SRAM, et on utilise les deux dans une seconde étape. Nous avions vu il y a quelques chapitre comment pipeliner des mémoires, dans le chapitre sur les mémoires évoluées. Et bien ces méthodes peuvent s'utiliser pour la mémoire RAM interne au cache !L'idée est d'insérer un registre entre la sortie de la RAM et la suite du cache, pour en faire un cache pipéliné, à deux étages. Le premier étage lit dans la SRAM, le second fait le reste. L'implémentation sur les caches associatifs à plusieurs voies est globalement la même à quelques détails près. [[File:Cache directement adressé pipeliné - deux étages.png|centre|vignette|upright=2|Cache directement adressé pipeliné - deux étages]] Avec le circuit précédent, il est possible d'aller plus loin, cette fois en pipelinant l'accès à la mémoire RAM interne au cache. Pour comprendre comment, rappelons qu'une mémoire SRAM est composée d'un plan mémoire et d'un décodeur. L'accès à la mémoire demande d'abord que le décodeur fasse son travail pour sélectionner la case mémoire adéquate, puis ensuite la lecture ou écriture a lieu dans cette case mémoire. L'accès se fait donc en deux étapes successives séparées, on a juste à mettre un registre entre les deux. Il suffit donc de mettre un gros registre entre le décodeur et le plan mémoire. [[File:Cache directement adressé pipeliné - trois étages.png|centre|vignette|upright=2|Cache directement adressé pipeliné - trois étages]] Et on peut aller encore plus loin en découpant le décodeur en deux circuits séparés. En effet, rappelez-vous le chapitre sur les circuits de sélection : nous avions vu qu'il est possible de créer des décodeurs en assemblant des décodeurs plus petits, contrôlés par un circuit de prédécodage. Et bien on peut encore une ajouter un registre entre ce circuit de prédécodage et les petits décodeurs. : Théoriquement, toute l'adresse est fournie d'un seul coup au cache, la quasi-totalité des processeurs présentent l'adresse complète à un cache pipeliné. Mais le Pentium 4 fait autrement. Il faut noter que les premiers étages manipulent l'indice dans la SRAM, qui est dans les bits de poids faible, alors que les étages ultérieurs manipulent le ''tag'' qui est dans les bits de poids fort. Les concepteurs du Pentium 4 ont alors décidé de présenter les bits de poids faible lors du premier cycle d'accès au cache, puis ceux de poids fort au second cycle. Pour cela, l'ALU fonctionnait à une fréquence double de celle du processeur, tout comme le cache L1. Il n'y avait pas de pipeline proprement dit, mais cela réduisait grandement la latence d'accès au cache. ===Le ''pipelining'' des caches splittés/sériels=== Il est aussi possible de pipeliner un cache dits splittés, aussi appelés à accès sériel. Pour rappel, les caches à accès sériel vérifient si il y a succès ou défaut de cache, avant d'accéder aux lignes de cache en cas de succès. Ils font donc différemment des autres caches, qui accèdent à une ligne de cache, avant de déterminer s'il y a succès ou défaut en lisant le tag de la ligne de cache. Les caches sériels disposent de deux SRAM : une pour les tags des lignes de cache et une pour les données. Ils accèdent à la SRAM pour les tags, avant d’accéder à la SRAM des données en cas de succès de cache. Vu que l'accès se fait en deux étapes, une vérification des tags suivie de la lecture/écriture des données, il est facile à pipeliner. Pipeliner le cache permet de régler le problème des accès au cache L1, et elle est tout le temps utilisé sur les processeurs modernes. Mais que faire en cas de défaut de cache ? ==Les caches non bloquants== Un '''cache bloquant''' est un cache auquel le processeur ne peut pas accéder pendant un défaut de cache. Il faut attendre que la lecture ou écriture en RAM soit terminée avant de pouvoir utiliser de nouveau le cache. Un '''cache non bloquant''' n'a pas ce problème : on peut l'utiliser pendant un défaut de cache. Les caches non bloquants permettent de démarrer une nouvelle lecture ou écriture alors qu'une autre est en cours, ce qui permet d'exécuter plusieurs lectures ou écritures en même temps. ===Les ''Miss Handling Status Registers''=== Lors d'un défaut de cache, la mémoire RAM est consultée pendant le défaut de cache, mais le cache est inutilisé. Un défaut de cache n'utilise pas le cache, ce n'est qu'un accès en mémoire RAM, sur le bus mémoire (ou un accès aux niveaux de cache inférieurs, peu importe). Le cache en lui-même est laissé libre, rien n’empêche d'y accéder, il est en réalité intrinsèquement non-bloquant. Les caches, bloquants comme non-bloquants, sont en réalité composés d'une mémoire cache proprement dite, entourée de circuits qui servent d'interface entre le processeur et le cache lui-même. Et parmi les circuits tout autour, certains gèrent l'accès au cache lors d'un défaut de cache. Ils sont regroupés sous le terme de '''''Miss Handling Architecture''''' (MHA). La différence entre un cache bloquant et un cache non-bloquant est en réalité liée à l'implémentation de la MHA. Les caches bloquants coupent volontairement l'accès au cache lors d'un défaut, car les défauts sont plus simples à gérer ainsi. Le défaut de cache rapatrie une donnée depuis la RAM, qui sera écrite dans le cache. Et il ne faut pas qu'une tentative d'accès à cette donnée ait lieu avant qu'elle ne soit chargée. Mais ce blocage est général et touche tout le cache, alors que seule une ligne de cache est concernée par le défaut de cache. L'idée derrière un cache non-bloquant est que seule la ligne de cache est bloquée, mais les autres sont accessibles. L'idée est alors de mémoriser les lignes de cache concernées par le défaut de cache, afin d'en bloquer l'accès. À chaque accès, on vérifie si la ligne de cache est déjà réservée par un défaut de cache. La lecture/écriture est alors bloquée si c'est le cas, mais elle accepte les accès sinon. Pour cela, la ''Miss Handling Architecture'' contient des registres qui mémorisent des informations sur les défauts de cache en cours. Ils portent le nom de '''''miss status handling registers''''', que j’appellerais dorénavant MSHR, qui sont aussi appelés des '''''miss buffer'''''. Le contenu des MSHR varie beaucoup suivant le processeur, mais ces derniers stockent au minimum les informations suivantes : * Le numéro de la ligne de cache dans laquelle les données sont chargées. * Un bit de validité qui indique si le MSHR est vide ou pas, qui est mis à 0 quand le défaut de cache est résolu. * Un ou plusieurs champs de lecture/écriture, qui contiennent des informations sur la lecture/écriture. ** Pour une lecture, elle contient des informations sur la destination de la donnée, à savoir qui prévenir quand le défaut de cache est terminé. C'est parfois un nom/numéro de registre (celui dans lequel charger la donnée), mais c'est souvent le numéro de l'entrée dans le ''load/store queue''. ** Pour les écritures, elle contient la donnée à écrire, ou éventuellement un numéro de ''load/store queue'' où se trouve la donnée à écrire. Il faut noter que le bus mémoire ne peut gérer qu'un seul défaut de cache à la fois. Aussi, il est intéressant de regarder ce qui se passe lorsqu'un second défaut de cache survient, pendant qu'un premier est en cours. Dans ce cas, il y a deux réponses qui correspondent à deux types de caches non-bloquants, qui portent les noms barbares de caches de type succès après défaut et défaut après défaut. Sur le premier type, il ne peut pas y avoir plusieurs défauts de cache simultanés. Dès qu'un second défaut de cache survient, le cache stoppe son activité et on ne peut plus démarrer de nouvelle lecture/écriture, tant que le premier défaut de cache n'est pas résolu. Le second type est plus souple et autorise la survenue de plusieurs défauts de cache simultanés. Du moins, jusqu'à une certaine limite, car le cache ne peut supporter qu'un nombre limité d'accès mémoires simultanés (pipelinés). ===Les accès simultanés à une même ligne de cache=== Il arrive que le processeur fasse plusieurs accès mémoire simultanés à la même ligne de cache. Si la ligne de cache en question n'a pas encore été chargée dans le cache, alors on a plusieurs défauts de cache consécutifs pour la même ligne de cache, et le cache non-bloquant doit gérer la situation. Pour la suite, il va falloir faire une petite distinction entre les défauts primaires et secondaires. Imaginons qu'un défaut de cache ait lieu et demande à charger une donnée dans la ligne de cache numéro N. Il s'agit du premier défaut impliquant cette ligne de cache précise, ce qui lui vaut le nom de '''défaut de cache primaire'''. Mais par la suite, d'autres accès mémoire à la même ligne de cache ont lieu, alors que la ligne de cache n'est pas encore disponible. Dans ce cas, il s'agit de '''défauts de cache secondaires'''. Pour l'unité d'accès mémoire, les défauts de cache primaire et secondaire sont différents (ils prennent tous une entrée dans la ''load/store queue''). Mais pour le cache, ils ne correspondent qu'à un seul accès au cache : celui qui demande de charger la ligne de cache demandée. Les défauts de cache primaire et secondaire à la même ligne de cache se voient attribuer un MSHR unique. La gestion des défauts de cache secondaires dépend du cache non-bloquant. La solution la plus simple ne permet pas les défauts de cache secondaires. Le cache ne permet pas deux défauts de cache simultanés pour la même ligne de cache. Les autres solutions le permettent, en fusionnant des accès simultanés à la même ligne de cache en un seul au niveau des MSHR. Dans tous les cas, détecter les défauts de cache secondaires sont un problème qu'il faut détecter. La ''Miss Handling Architecture'' doit détecter les défauts de cache secondaire. Pour cela, elle procède comme suit. Lors de chaque défaut de cache, la MHA récupère le numéro de la ligne de cache associé. Il vérifie alors chaque MSHR pour vérifier s'il contient le numéro en question. S'il n'y a aucune correspondance dans les MSHR, alors c'est signe que le défaut de cache est un défaut primaire. Mais s'il y en a une, alors c'est un défaut secondaire. Évidemment, cela signifie que lors d'un défaut de cache, le numéro de ligne de cache est envoyé à tous les MSHR, pour comparaison. Les MSHR sont donc regroupés dans une mémoire associative, une sorte de mini-cache, faciliter l'implémentation. Lors d'un défaut de cache primaire, l'accès à un cache non-bloquant se fait comme suit : le processeur envoie une adresse au cache, accède à celui-ci, et détecte la survenue d'un défaut de cache. Il en profite alors pour attribuer une ligne de cache dans laquelle sera chargée ce défaut. L'attribution est très simple dans le cas des caches ''direct mapped'', ou associatifs par voie, pour lesquels l'attribution se fait assez simplement. Il mémorise alors cette information dans les MSHR, après avoir vérifié que le défaut de cache n'était pas un défaut secondaire. Lors des accès ultérieurs à une adresse proche, censée être dans la même ligne de cache, le processeur va encore une fois rencontrer un défaut de cache. Il va alors déterminer le numéro de la ligne de cache associée à l'adresse, et comparer ce numéro avec les MSHRs. Si un MSHR contient ce numéro, c'est signe que le défaut de cache est un défaut secondaire. La MHA réagit alors différemment selon le processeur considéré. Une première solution n'autorise pas les défauts de cache secondaires. Si l'un d'entre eux survient, le processeur est gelé par un ''pipeline stall'', une bulle de pipeline. Une autre solution fusionne les défauts de cache secondaires avec le défaut de cache primaire : tout cela ne correspond qu'à un seul défaut de cache pour lui. ===Les MSHR simples=== Un cache non-bloquant à '''MSHR simple''' contient juste plusieurs MSHR qui mémorisent juste un numéro de ligne de cache, un bit de validité, le champ de lecture/écriture, et l'adresse exacte de lecture/écriture. Avec cette organisation, il est possible d'avoir plusieurs défauts de cache séparés, mais à la condition que chaque défaut accède à une ligne de cache différente. Deux accès simultanés à une même ligne de cache ne sont pas possibles, les défauts de cache secondaires ne sont pas autorisés. Ainsi, chaque défaut de cache se voit attribuer son propre MSHR, chacun contient un numéro de ligne de cache différent. Pour comprendre pourquoi c'est impossible de gérer les défauts secondaires, il faut regarder le champ de lecture/écriture. Si on veut effectuer plusieurs écritures consécutives à la même adresse, le MSHR n'aura pas de quoi mémoriser les deux données à écrire. Il pourra mémoriser la première donnée à écrire, pas la seconde. Même chose lors d'une lecture : le champ lecture/écriture peut mémorisr la destination de la première lecture, pas de la seconde. L'avantage est que la MHA n'a besoin que des MSHR et de quelques circuits annexes. Les autres solutions rajoutent des circuits annexes pour gérer les défauts de cache secondaires, qui utilisent beaucoup de circuits. Le cout en circuit est donc élevé, mais le gain en performance est là. Passons maintenant aux caches non-bloquants qui autorisent les défauts de cache secondaire. La solution la plus simple consiste à utiliser ===Les MSHR adressés implicitement=== Avec les '''MSHR adressés implicitement''', il est possible de fusionner plusieurs accès mémoire à une même ligne de cache, mais sous une condition très importante : ces accès lisent/écrivent des mots mémoire différents. Par exemple, imaginons qu'une ligne de cache contienne 8 mots mémoire de 64 bits. Si un premier accès mémoire lit le mot mémoire numéro 7 (dernier mot mémoire de la ligne), et le second accès le mot mémoire numéro 3, alors la fusion est possible. Mais si deux accès mémoire veulent lire/écrire le mot mémoire numéro 7, alors la fusion n'est pas possible et le processeur se bloque, un ''pipeline stall'' survient. Un MSHR adressé implicitement est un MSHR simple, qui contient naturellement un numéro de ligne de cache (tag) et un bit de validité, sauf que le champ de lecture/écriture est dupliqué. Les différents champs lecture/écriture d'un MSHR sont regroupés dans une mémoire RAM/cache qui contient autant d'entrées qu'il y a de mot mémoire dans une ligne de cache. Chaque entrée de la table est associée à un mot mémoire de la ligne de cache et stocke des informations sur celui-ci. Une entrée mémorise, au minimum : * Un champ de lecture/écriture qui contient soit la destination de la lecture, soit la donnée à écrire. * Un bit R/W permettant d'interpréter correctement le champ de lecture/écriture. * Un bit de validité pour chaque entrée de la table, qui dit si un défaut de cache antérieur accède déjà à ce mot mémoire. La fusion de deux défauts de cache est ainsi assez simple. Un défaut de cache primaire/secondaire configure l'entrée associée au mot mémoire lu/écrit. Si un défaut secondaire ultérieur lit/écrit un mot mémoire différent (dont le bit de validité de l'entrée dans la table est à zéro), pas de conflit : il configure une autre entrée, vide. Mais s'il lit/écrit un mot mémoire pour lequel l'entrée de la table est occupée, il y a conflit, le processeur est bloqué par un ''pipeline stall'', une bulle de pipeline. Avec cette organisation, le nombre de MSHR indique combien de lignes de cache peuvent être lues en même temps depuis la mémoire. Quant au nombre d'entrées par MSHR, il détermine combien d'accès mémoires qui ne se recouvrent pas peuvent avoir lieu en même temps. ===Les MSHR adressés explicitement=== Les '''MSHR adressés explicitement''' n'ont pas les contraintes des MSHR adressés implicitement. Avec eux, il est possible d'avoir plusieurs défauts de cache pointant vers la même ligne de cache, mais aussi vers le même mot mémoire. De tels défauts de cache apparaissent sur les processeurs à exécution dans le désordre, mais sont très rares, voire inexistants, sur les processeurs ''in-order''. L'idée est encore que chaque MSHR est associée à une table mémoire qui mémorise des entrées. Cette table n'est autre qu'une mémoire FIFO. Sauf que cette fois-ci, une entrée n'est pas associée à un mot mémoire. Une entrée est associée à un défaut de cache. Une entrée mémorise là encore la destination de la lecture ou la donnée de l'écriture suivi du bit R/W, ainsi que d'un bit de validité par entrée, mais aussi : la position du mot mémoire lu dans la ligne de cache. C'est cette dernière information qui n'était pas présente dans les MSHR adressés implicitement. De plus, le nombre d'entrée par MSHR n'est pas égal au nombre de mots mémoires dans une ligne de cache, mais peut être arbitrairement grand. L'avantage d'un tel cache est qu'il est capable de traité les défauts secondaires concernant un même mot mémoire, et ce, car les accès à la ligne de cache concernée se produisent dans l'ordre de génération des défauts (la table mémoire est une FIFO). ===Les MSHRs inversés=== Généralement, plus on veut supporter de défauts de cache, plus le nombre de MSHR et d'entrées augmente. Mais au-delà d'un certain nombre d'entrées et de MSHR, les MSHR adressés implicitement et explicitement ont tendance à bouffer un peu trop de circuits. Utiliser une organisation un peu moins gourmande en circuits est donc une nécessité. Cette organisation plus économe se base sur des MSHR inversés. Les MSHR inversés ne contiennent qu'une seule entrée, en quelque sorte : au lieu d’utiliser n MSHR de m entrées chacun, on va utiliser n × m MSHR inversés. La différence, c'est que plusieurs MSHR peuvent contenir un tag identique, contrairement aux MSHR adressés implicitement et explicitement. Lorsqu'un défaut de cache a lieu, chaque MSHR est vérifié. Si jamais aucun MSHR ne contient de tag identique à celui utilisé par le défaut, un MSHR vide est choisi pour stocker ce défaut, et une requête de lecture en mémoire est lancée. Dans le cas contraire, un MSHR est réservé au défaut de cache, mais la requête n'est pas lancée. Quand la donnée est disponible, les MSHR correspondant à la ligne qui vient d'être chargée vont être utilisés un par un pour résoudre les défauts de cache en attente. ===Les MSHR intégrés au cache=== Certains chercheurs ont remarqué que pendant qu'une ligne de cache est en train de subir un défaut de cache, celle-ci reste inutilisée, et son contenu est destiné à être perdu une fois le défaut de cache résolu. Ils se sont dit que, plutôt que d'utiliser des MSHR séparés, il vaudrait mieux utiliser la ligne de cache pour stocker les informations sur les défauts de cache en attente dans cette ligne de cache. Pour éviter tout problème, il faut rajouter un bit dans les bits de contrôle de la ligne de cache, qui sert à indiquer que la ligne de cache est occupée : un défaut de cache a eu lieu dans cette ligne, et elle stocke donc des informations pour résoudre les défauts de cache. ==L'entrelacement mémoire== Nous venons de voir comment une mémoire cache peut gérer plusieurs accès mémoire simultanés. Il se trouve que ce genre d'optimisation dépasse largement le cadre du cache. Les mémoires RAM ne sont pas en reste, elles aussi ! Il est possible de pipeliner l'accès à une mémoire RAM, en utilisant des techniques dites d''''entrelacement'''. Précisons cependant que cette optimisation n'est pas utilisée sur les mémoires SDRAM ou DDR modernes, du moins pas sans modifications majeures. Mais divers ordinateurs assez anciens, dont des superordinateurs, ont utilisé cette technique. La technique demande d'utiliser plusieurs mémoires séparées. Sur les anciens ordinateurs, les mémoires en question sont des chips mémoire, à savoir des circuits intégrés de DRAM. Pour faire plus général, nous allons utiliser le terme de '''banques''', ou encore de bancs mémoire. La différence est qu'il existe des mémoires multi-banque, qui regroupent plusieurs banques indépendantes dans un seul boitier, dans un seul chip mémoire. En clair, de telles mémoires regroupent plusieurs sous-mémoires dans un seul circuit intégré. Il est aussi possible d'utiliser plusieurs chips mémoire séparés, chacun ayant une banque. Reste à combiner ces banques pour former un pseudo-pipeline. ===Utiliser plusieurs banques sans entrelacement=== Sans optimisation particulière, les adresses sont réparties dans les banques comme indiqué ci-dessous. Il s'agit de ce qui s'appelle un arrangement horizontal, et nous avions vu celui-ci dans le chapiutre sur les mémoires SDRAM. [[File:Répartition des adresses sans entrelacement.png|centre|vignette|upright=1.5|Répartition des adresses sans entrelacement.]] Avec cet arrangement, les bits de poids fort de l'adresse sont utilisées pour sélectionner la banque adéquate, et le reste de l'adresse est envoyé sur le bus d'adresse. Les adresses mémoire sont alors découpées comme suit : {|class="wikitable" |+ Adresse mémoire |- ! Adresse de banque !! Adresse dans la banque |- | Quelques bits de poids fort || Reste de l'adresse |} L'entrelacement change cette répartition, afin d'accélérer les accès mémoire. L'entrelacement classique vise à améliorer les accès à des adresses mémoires consécutives, mais des formes plus évoluées d'entrelacement visent à optimiser des accès mémoire arbitraires. Voyons-les dans l'ordre, du plus simple au plus complexe. ===L'entrelacement classique=== Avec l''''entrelacement classique''', des adresses consécutives sont réparties dans des banques consécutives. Précisons que cette attribution des adresses n'implique absolument pas la mémoire virtuelle ou n'importe quel mécanisme dans le processeur. La répartition décide que telle adresse mémoire va dans telle banque, à telle adresse dans la banque. Elle est donc le fait du contrôleur mémoire, donc en dehors du processeur (les contrôleurs mémoires n’étaient pas encore intégrés dans le processeur à l'époque). [[File:Répartition des adresses dans une mémoire interleaved.png|centre|vignette|upright=1.5|Répartition des adresses dans une mémoire interleaved.]] L'entrelacement simple permet d'accélérer les accès mémoire qui se font à des adresses consécutives. Avec l'entrelacement, chaque accès mémoire tombe dans une banque mémoire différente, ce qui fait qu'on peut démarrer un nouvel accès mémoire à chaque cycle d'horloge. Pendant qu'une banque est occupée par un accès mémoire, on démarre le suivant dans une autre banque, et ainsi de suite. Le tout se termine soit quand on a épuisé toutes les banques libres, soit quand l'accès en rafales se termine. Pas besoin d'attendre que la mémoire ait fini sa lecture/écriture avant de démarrer la lecture/écriture suivante. [[File:Pipemining mémoire.png|centre|vignette|upright=2.5|Pipelining mémoire]] L'animation suivante illustre bien le processus, avec quatres banques. [[File:Interleaving.gif|centre|vignette|upright=2.5|Pipelining mémoire]] L'entrelacement simple utilise les bits de poids faible pour sélectionner la banque, et les bits de poids fort pour la case mémoire. [[File:Adresse mémoire d'une mémoire entrelacée.png|centre|vignette|upright=2|Adresse mémoire d'une mémoire entrelacée]] Précisons que le temps d'accès mémoire ne change pas beaucoup avec l'entrelacement. Par contre, on peut faire plus d'accès mémoire simultanés. On peut démarrer un accès mémoire par cycle d'horloge, mais l'accès en lui-même prend plusieurs cycles. Les mémoires à entrelacement ont donc un débit supérieur aux mémoires qui ne l'utilisent pas. L'entrelacement classique pourrait être utilisé pour accélérer les transferts entre mémoire RAM et cache, qui se font en blocs d'adresses consécutives. Mais de nos jours, il n'est pas utilisé dans cette optique. Il faut dire qu'il est redondant avec le mode rafale des mémoires DRAM. Les deux font globalement la même chose et on voit mal comment utiliser les deux en même temps. Le mode rafale étant plus simple à implémenter, et plus léger niveau utilisation du bus de commande mémoire, il est préféré à l'entrelacement. Par contre, l'entrelacement a eu son heure de gloire sur d'anciens processeurs qui n'avaient pas de mémoire cache, alors qu'ils disposaient d'un pipeline. L'exemple typique est celui des nombreux ''processeurs vectoriels'', que nous n'avons pas encore abordé à ce stade du cours. De tels processeurs avaient un pipeline, une unité de calcul fortement pipelinée, et parfois même de l'exécution dans le désordre et du renommage de registres ! ===L'entrelacement par décalage=== L'entrelacement simple est très utile pour les accès en rafle ou équivalents. Par contre, il existe de rares situations où il n'est pas optimal. Une de ces situations est celle des accès en enjambée (en ''stride''), où l'on accède en série à des adresses sont séparées par N mots mémoires. [[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]] De tels accès surviennent quand un logiciel accède à des structures de données spécifiques, à savoir des tableaux de structures, des matrices, ou d'autres structures de données dans le genre. Un cas classique est celui du parcours d'une matrice colonne par colonne. Les matrices de nombres sont mémorisées ligne par ligne, ce qui fait que parcourir une colonne demande de passer d'une ligne à la suivante et de faire des grands sauts en mémoire RAM, mais tous espacés par la même distance. Avec l'entrelacement simple, les accès en enjambée sont moins performants que les accès à des adresses consécutives, mais cela ne fonctionne pas trop mal. Le pire cas est celui où l'on a N banques et où les données sont justement placées toutes les N adresses. Des accès consécutifs vont tous tomber dans la même banque, on ne peut plus accéder à des banques différentes en parallèle. Mais en dehors de ce cas, on voit une amélioration par rapport à la situation sans entrelacement. Pour obtenir des performances maximales pour les accès en enjambées, il faut répartir les mots mémoires dans la mémoire autrement. L'organisation idéale est la suivante. : Dans les explications qui vont suivre, la variable N représente le nombre de banques, qui sont numérotées de 0 à N-1. On commence par organiser les N premières adresses comme une mémoire entrelacée simple : l'adresse 0 correspond à la banque 0, l'adresse 1 à la banque 1, etc. Pour le bloc suivant, on décale tout d'une adresse, à savoir qu'on commence à remplir les banques à partir de la seconde, non de la première. Une fois la fin du bloc atteinte, on finit de remplir le bloc en repartant du début du bloc. Le troisième bloc d'adresse subit le même traitement, sauf qu'on commence à remplir à partir de la seconde banque. Et on poursuit l’assignation des adresses en décalant d'un cran en plus à chaque bloc. Ainsi, chaque bloc verra ses adresses décalées d'un cran en plus comparé au bloc précédent. Si jamais le décalage dépasse la fin d'un bloc, alors on reprend au début. [[File:Mémoire interleaved par décalage.png|centre|vignette|upright=1.5|Mémoire entrelacée par décalage.]] En faisant cela, on remarque que les banques situées à N adresses d'intervalle sont différentes. Dans l'exemple du dessus, nous avons ajouté un décalage de 1 à chaque nouveau bloc à remplir. Mais on aurait tout aussi bien pu prendre un décalage de 2, 3, etc. Dans tous les cas, on obtient un entrelacement par décalage. Ce décalage est appelé le pas d'entrelacement, noté P. Le calcul de l'adresse à envoyer à la banque, ainsi que la banque à sélectionner se fait en utilisant les formules suivantes : * adresse à envoyer à la banque = adresse totale / N ; * numéro de la banque = (adresse + décalage) modulo N ; * décalage = (adresse totale * P) mod N. Avec cet entrelacement par décalage, on peut prouver que la bande passante maximale est atteinte si le nombre de banques est un nombre premier. Seulement, utiliser un nombre de banques premier peut créer des trous dans la mémoire, des mots mémoires inadressables. Malgré ce défaut, la technique a été utilisée sur quelques ordinateurs, avec l'exemple notable du superordinateur ''Burroughs Scientific Processor''. Pour éviter cela, il y a plusieurs solutions. Par exemple, on peut faire en sorte que N et la taille d'une banque soient premiers entre eux : ils ne doivent pas avoir de diviseur commun. Mais en pratique, elles n'ont pas vraiment été implémentées dans une vraie machine, et sont restées à l'état de recherche, aussi je les passe sous silence. ===L'entrelacement pseudo-aléatoire=== Une dernière méthode de répartition consiste à répartir les adresses dans les banques de manière "pseudo-aléatoire". L'idée exacte est de faire passer l'adresse dans une '''fonction de hachage''', plus ou moins complexe. Pour rappel, une fonction de hachage prend une entrée de grande taille et fournit en sortie un résultat de petite taille. Idéalement, elles doivent donner des résultats aussi différents que possible pour des entrées similaires, histoire de simuler une sorte de pseudo-aléatoire. Ici, une adresse est transformée en un numéro de banque. Une fonction de hachage simple ne fait que permuter des bits de l'adresse pour obtenir son résultat. Elle ne fait qu'échanger des bits de place avant de couper l'adresse en deux morceaux : un pour la sélection de la banque, et un autre pour la sélection de l'adresse dans la banque. Cette permutation est fixe, et ne change pas suivant l'adresse. Des fonctions de hachage plus complexes font des XOR bit à bit entre certains bits de l'adresse. ==Les contrôleurs SDRAM/DDR avec "exécution dans le désordre"== Après avoir vu le cas des mémoires RAM, nous allons nous concentrer sur le cas particulier des mémoires SDRAM. Les mémoires SDRAM et DDR modernes sont capables de gérer plusieurs accès simultanés à la mémoire RAM, et leurs contrôleurs mémoire en font tout autant. C'est une différence majeure avec les mémoires asynchrones FPM/EDO, qui n'acceptaient qu'un seul accès mémoire à la fois. Leur contrôleur mémoire n'acceptait qu'un seul accès mémoire à la fois, c'était un contrôleur mémoire bloquant. Les contrôleurs mémoires des SDRAM sont eux non-bloquants et peuvent encaisser une dizaine d'accès mémoire à la fois. Peu de choses sont connues sur les contrôleurs de SDRAM/DDR modernes, les fabricants ne donnant que peu de détails dessus. Les rares simulateurs qui tentent de décrire leur fonctionnement, comme DRAM SIM I et II, sont particulièrement simples et ne vont pas dans le détail. Néanmoins, le peu qu'on sait est tout de même instructif. ===Rappel sur les SDRAM : tampon de ligne et commandes=== Les mémoires SDRAM sont des mémoires à tampon de ligne. Elles font un accès mémoire en deux étapes. La première étape recopie une ligne de N * 64 bits dans un tampon interne à la SDRAM. La seconde étape sélectionne une colonne, à savoir une donnée de 64 bits, qui est soit envoyée sur le bus de données pour une lecture, soit modifiée par une écriture. [[File:Mémoire à tampon de ligne.png|centre|vignette|upright=2|Mémoire à tampon de ligne]] Les accès mémoire sont traduits par un séquenceur mémoire en une série de commandes mémoires, qui sont séparées par des délais mémoire de quelques cycles d'horloge. Les délais sont très précis, et sont à respecter à la lettre. Une lecture ou une écriture se fait en maximum trois commandes : une commande PRECHARGE qui ferme la ligne précédemment utilisée, une commande ACT qui précise l'adresse de la ligne, et une commande READ ou WRITE qui précise l'adresse de la colonne et éventuellement la donnée à écrire. : Pour être plus précis, la commande PRECHARGE précharge les lignes de bits du plan mémoire à une tension particulière, ce qui les vide de leur contenu. Mais c'est un détail sans importance pour ce qui va suivre. Les SDRAM permettent de se passer de la première étape si des accès consécutifs se font dans la même ligne. Une fois activée, la ligne reste ouverte et on peut accéder plusieurs fois de suite dedans. On a alors juste à préciser l'adresse de colonne dedans. Si un accès mémoire accède à une ligne déjà activée, on dit que c'est un '''succès de page'''. Si ce n'est pas le cas, on doit fermer la ligne courante, rouvrir la ligne voulue, et préciser la colonne. C'est alors un '''défaut de page'''. {|class="wikitable" |- ! !! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 !! Cycle 10 !! Cycle 11 !! Cycle 12 !! Cycle 13 |- ! Bus de commande/adresse | bgcolor="#FFA0FF" | PRECHARGE || bgcolor="#FFA0FF" | ACT || || bgcolor="#A0FFFF" | READ (1) || || bgcolor="#A0FFFF" | READ (2) || bgcolor="#A0FFFF" | READ (3) || || || bgcolor="#A0FFFF" | READ (4) || bgcolor="#A0FFFF" | READ (5) || || |- ! Bus de données | || || || || || bgcolor="#A0FFFF" | Donnée READ 1 || ||bgcolor="#A0FFFF" | Donnée READ 2 | bgcolor="#A0FFFF" | Donnée READ 3 || || || bgcolor="#A0FFFF" | Donnée READ 4 || bgcolor="#A0FFFF" | Donnée READ 5 |} Les SDRAM sont parfois capables de démarrer une commande avant que la précédente soit terminée. Mais le respect des délais mémoire est très limitant, ce qui fait qu'on ne peut pas parler de réel pipeline, comme c'est le cas sur les processeurs. Parlons plutôt de '''commandes anticipées'''. {|class="wikitable" |- ! !! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 !! Cycle 10 !! Cycle 11 !! Cycle 12 !! Cycle 13 |- ! Bus de commande/adresse | || bgcolor="#A0FFFF" | ACT || || bgcolor="#A0FFFF" | READ (1) || || || || bgcolor="#FFA0FF" | READ (2) || || || || || |- ! Bus de données | || || || || || bgcolor="#A0FFFF" | READ 1 || bgcolor="#A0FFFF" | READ 1 || bgcolor="#A0FFFF" | READ 1 || bgcolor="#A0FFFF" | READ 1 | bgcolor="#FFA0FF" | READ 2 || bgcolor="#FFA0FF" | READ 2 || bgcolor="#FFA0FF" | READ 2 || bgcolor="#FFA0FF" | READ 2 | |} Un dernier point est que les chips mémoires de SDRAM sont composés de plusieurs banques, chacune étant une sorte de mini-mémoire miniature. Chaque banque a son propre décodeur, son propre tampon de ligne, ses propres multiplexeurs de colonne, sa logique de rafraichissement mémoire, etc. C'est comme si une SDRAM regroupait plusieurs mémoires séparées dans un même circuit intégré. [[File:Arrangement vertical.jpg|centre|vignette|upright=2.5|Mémoire multi-banques.]] ===La mise en attente des accès mémoire=== Un contrôleur de SDRAM peut accepter plusieurs accès mémoire en même temps. Mais cela ne signifie pas que le contrôleur sera prêt à les traiter immédiatement. Pour éviter tout problème de disponibilité, le contrôleur met en attente les accès mémoire que le processeur lui envoie, pour les exécuter dès que possible. Les accès mémoire sont mis en attente dans une mémoire FIFO, histoire de les exécuter dans leur ordre d'arrivée, notamment pour renvoyer les lectures dans l'ordre demandé. Il y a aussi une mémoire FIFO pour les données à écrire et une FIFO pour les données lues. Cette dernière sert au cas où le cache ne soit pas disponible quand on lui envoie la donnée lue. [[File:Module d'interface avec la mémoire.png|centre|vignette|upright=2.5|Contrôleur mémoire avec mise en attente des requêtes processeur et autres optimisations.]] ===Les lectures anticipées : une forme d'OOO mémoire=== La mise en attente des accès mémoire est une optimisation intéressante, mais pas mirifique. Elle permet surtout de ne pas bloquer le processeur si le contrôleur mémoire a du travail sur la planche. Cependant, elle peut être optimisée quand on se rend compte d'une chose : le processeur ne voit que les lectures, pas les écritures. Pour les écritures, il les envoie au contrôleur mémoire et ce dernier fait le travail demandé, le processeur n'est pas prévenu quand une écriture se termine. Mais pour les lectures, c'est différent, car il reçoit la donnée lue. En conséquence, il est possible d'optimiser les écritures sans que le processeur ne voie quoique ce soit. Une optimisation possible est alors de changer l'ordre des accès mémoire, histoire de retarder les écritures ou au contraire de les faire en avance. La raison est que les mémoires SDRAM n'aiment pas quand on alterne lectures et écritures. Les délais mémoire, les fameux ''timings'' mémoire, sont clairs : faire une suite de lecture ou une suite d'écriture est plus performant que d'alterner entre lectures et écritures. L'idée est alors de changer l'ordre des écritures de manière à les faire en bloc, idem pour les lectures. L’optimisation est possible, mais à condition que le processeur n'y voie que du feu. Or, vous l'avez deviné, il y a des cas où ces réorganisations ne sont pas possibles, et vous avez sans doute trouvé que la situation était familière. L'optimisation en question est une forme d'exécution dans le désordre des accès mémoire, réalisée par le contrôleur mémoire lui-même ! Et qui dit exécution dans le désordre dit : problèmes liés aux dépendances de données. Changer l'ordre des écritures n'est pas toujours possible, notamment en raison des dépendances RAW, WAR et WAW. Pour que le processeur n'y voie que du feu, il faut respecter plusieurs critères. Premièrement, les lectures doivent être renvoyées au processeur dans l'ordre. Et quand je dis dans l'ordre, cela veut dire : dans l'ordre d'envoi des lectures au contrôleur mémoire. Les écritures aussi doivent se faire dans l'ordre, histoire qu'une série d'écriture donne le bon résultat final. Le troisième critère est qu'une lecture doit donner le résultat de la dernière écriture ''à la même adresse''. Pour le dire autrement, il faut juste détecter les dépendances RAW. Concrètement, si on a une série de lectures et d'écritures alternées, on peut regrouper les lectures et les écritures dans deux paquets séparés, à condition qu'aucune lecture ne lise une adresse écrite par une écriture. Mais si une lecture est dépendante d'une écriture, alors la lecture doit attendre que l'écriture se termine, idem pour les lectures suivantes. Une implémentation basique remplace la mémoire FIFO vue au-dessus, par deux mémoires FIFOs : une pour les lectures, une pour les écritures. Lorsqu'un accès mémoire arrive au séquenceur, il regarde si c'est une lecture ou une écriture et place l'accès mémoire dans la file adéquate. Le fait que ce soit des FIFOs garantit que les lectures se font dans l'ordre, idem pour les écritures. En sortie des deux FIFOs, le séquenceur mémoire détecte les dépendances RAW. Précisément, quand une lecture sort de la mémoire FIFO, il consulte la file d'attente des écritures pour voir si la lecture est dépendante d'une écriture en attente. La lecture attend si c'est le cas, et les écritures sont exécutées à la place. Séparer lectures et écriture est une source d'optimisation. Il est par exemple possible d'utiliser le réacheminement lecture sur écriture (''Store to load forwarding''). Il s'agit d'un équivalent de l’optimisation du même nom, utilisée dans le cadre de la désambiguïsation mémoire. Imaginez qu'une lecture accède à une donnée pas encore écrite, en attente dans le contrôleur mémoire. C'est une dépendance RAW assez claire. Dans ce cas, le contrôleur mémoire peut renvoyer la donnée directement depuis ses mémoires FIFOs, sans faire d'accès mémoire en lecture. Le contrôleur mémoire a tendance à privilégier les lectures, car celles-ci sont cruciales pour l'exécution dans le désordre. Plus elles se font tôt, plus vite le processeur recevra les données et pourra démarrer les instructions dépendantes de cette donnée. Le séquenceur a donc tendance à piocher en priorité dans la file de lecture, plutôt que dans la file d'écriture. À vrai dire, dans le cas idéal, les écritures ne sont faites que quand la file d'écriture est pleine ou quasi pleine... Prenons l'exemple des coprocesseurs IO 81341 et 81342, qui étaient en réalité des ''chipsets'' intégrant un contrôleur SDRAM. Le ''chipset'' avait 5 ports : un pour les processeurs, un pour le pont sud (''southbridge'') et trois pour des canaux DMA. Pour le port processeur, il n'y avait pas de réordonnancement et ce port utilisait une seule mémoire FIFO. Les autres ports utilisaient l'optimisation qu'on vient de voir, et avaient vraisemblablement des files séparées pour les lectures et écritures. Le séquenceur mémoire vérifiait les dépendances mémoire de type RAW et autres. [[File:Double file d'attente pour les lectures et écritures.png|centre|vignette|upright=2.5|Double file d'attente pour les lectures et écritures]] ===Le ré-ordonnancement des commandes mémoires=== L'optimisation précédente est peu poussée, comparé aux formes d'exécution dans le désordre que les processeurs utilisent. Et on peut se demander si une exécution dans le désordre plus poussé est possible. La réponse est oui : un contrôleur mémoire peut faire des réorganisations bien plus poussées. Par contre, il faut faire une précision très importante : le contrôleur mémoire ne remet pas les accès mémoire dans l'ordre avant de les envoyer au processeur. C'est le processeur qui remet en ordre les accès mémoire, dans sa ''load queue''. En clair : le processeur voit les accès mémoire dans le désordre, c'est lui qui les remet en ordre. Pour cela, les requêtes mémoire sont "numérotées", à savoir qu'on leur attribue un identifiant binaire. Le contrôleur mémoire exécute les lectures/écritures dans le désordre, mais garde la trace de leur identifiant. Il sait dans quel ordre il exécute les accès mémoire et connait leur latence. Ce qui fait qu'il sait que la donnée lue à tel instant est associée à la requête mémoire numéro X. Pour les lectures, il renvoie la donnée lue avec l'identifiant. Le processeur sait alors à quelle lecture la donnée lue correspond et il se débrouille en interne pour gérer la situation. Maintenant que ces précisions sont faites, posons cette question : dans quelles situations est-il pertinent de faire des accès mémoire dans le désordre ? La réponse est : quand plusieurs accès à une même ligne ne sont pas consécutifs, qu'il y a des accès entre les deux. Après ré-ordonnancement, ces accès mémoire à une même ligne sont exécutés l'un à la suite de l'autre, ce qui est beaucoup plus rapide. Pour rendre le tout plus concret, voici un exemple. Imaginez que l'on sait les 3 accès mémoire suivants : * Une lecture ligne A ; * PRECHARGE + ACT ; * Une écriture ligne B ; * PRECHARGE + ACT ; * Une lecture ligne A. Sans réordonnancent, on doit émettre deux commandes PRECHARGE quand on passe d'une ligne à l'autre, et il faut ajouter les commandes ACT avec. Pour éviter cela, le contrôleur mémoire peut retarder l'écriture, ou au contraire avancer la seconde lecture. Le résultat est alors le suivant : * Lecture ligne A ; * Lecture ligne A * PRECHARGE + ACT ; * Une écriture ligne B. On a donc deux accès consécutifs à la même ligne, suivi par une écriture dans une autre ligne. La technique marche parce que l'on a un mix adéquat de lectures et d'écritures. Et encore une fois, il faut éviter d'intervertir lectures et écritures à une même adresse, sous peine de problèmes. Encore une fois, les dépendances RAW posent problème ! L'implémentation est assez simple : la ou les mémoires FIFOs précédentes sont remplacées par des mémoires similaires aux ''schedulers''. Le contrôleur mémoire vérifie à chaque cycle les accès en attente, et vérifie à quelle ligne ils accèdent. Il priorise alors les accès qui tombent dans la ligne ouverte : ceux-là sont exécutés avant les autres. S'il n'y en a pas, il prend l'accès mémoire le plus ancien (ordre FIFO). Il teste aussi les dépendances mémoires RAW avant d'envoyer des commandes à la mémoire DDR/SDRAM. une telle solution est appelée l''''algorithme ''FR-FCFS''''' (''First Ready-First Come First Serve''). ===Les optimisations liées à la présence de plusieurs banques=== Les optimisations précédentes sont décuplées par la présence de plusieurs banques dans la mémoire SDRAM. Il est en effet possible de faire plusieurs accès mémoire en même temps, dans des banques différentes. Il est possible de lancer un accès mémoire dans deux banques en même temps, par exemple. La seule contrainte est que la SDRAM est limitée à 4 banques actives en même temps. {|class="wikitable" |+ Pipelining basique sur les SDRAM |- ! !! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 !! Cycle 10 !! Cycle 11 |- ! Banque Numéro 1 | || || || colspan="3" bgcolor="#FFFFA0" | Accès mémoire || colspan="3" bgcolor="#FFFFA0" | Accès mémoire || || |- ! Banque Numéro 2 | || colspan="3" bgcolor="#A0FFFF" | Accès mémoire || || || colspan="3" bgcolor="#A0FFFF" | Accès mémoire || || |- ! Banque Numéro 3 | || || || colspan="3" bgcolor="#FFA0FF" | Accès mémoire || || colspan="3" bgcolor="#FFA0FF" | Accès mémoire || |} Les optimisations précédentes peuvent s'appliquer par banque. Il est par exemple possible d'utiliser une mémoire FIFO par banque. Ainsi, les accès dans une même banque se feront en série, dans l'ordre d'arrivée, mais des accès à des banques différentes se feront dans le désordre. Cela ne pose pas de problème, car les accès se font à des adresses différentes, ce qui fait qu'il n'y a pas de conflits majeurs. Il y a cependant un risque que les lectures se fassent dans le désordre si on n'y prend pas garde. [[File:Gestion parallèle des banques.png|centre|vignette|upright=2.5|Gestion parallèle des banques.]] Il est aussi possible d'utiliser une file séparée pour les lectures et les écritures, pour chaque banque. Les performances sont alors améliorées comparé à deux files globales pour toute la SDRAM. En idem avec la réorganisation des accès mémoire, pour regrouper les accès à une même ligne. Un problème avec ces optimisations est qu'il est rare que des accès mémoire se fassent dans des banques séparées. Les accès mémoire tendent à se faire avec une bonne localité spatiale, ce qui fait qu'ils tombent dans une même banque. Heureusement, les contrôleurs SDRAM/DDR modernes incorporent des optimisations pour corriger ce problème. Les optimisations en question sont une forme d'entrelacement adaptée aux mémoires SDRAM. L'entrelacement naïf ne marche pas à cause de la présence du tampon de ligne. Cependant, il peut y avoir un entrelacement entre banques SDRAM. Voyons ce que ça veut dire. L''''entrelacement de banques''' répartit deux lignes consécutives dans deux banques différentes. Pour comprendre l'idée, prenons un exemple. Imaginons une mémoire avec deux banques et 4 lignes. Imaginons qu'on parcoure/balaye la mémoire RAM en partant des adresses basses. Sans entrelacement de ligne, les accès se feront comme suit, de gauche à droite : {|class="wikitable" |+ Adresse mémoire |- ! colspan="4" | Banque numéro 1 !! colspan="4" | Banque numéro 2 |- | Ligne 1 || Ligne 2 || Ligne 3 || Ligne 4 || Ligne 1 || Ligne 2 || Ligne 3 || Ligne 4 |} Avec l'entrelacement de banques, les accès se feront comme suit : {|class="wikitable" |+ Adresse mémoire |- ! Banque n°1 !! Banque n°2 !! Banque n°1 !! Banque n°2 !! Banque n°1 !! Banque n°2 !! Banque n°1 !! Banque n°2 |- | Ligne 1 || Ligne 1 || Ligne 2 || Ligne 2 || Ligne 3 || Ligne 3 || Ligne 4 || Ligne 4 |} L'avantage est que passer d'une ligne à la suivante est plus rapide. Pas besoin de fermer la ligne pour passer à la suivante, on ouvre directement la ligne suivante dans une banque différente, ce qui sera plus rapide. La commande PRECHARGE pourra être envoyée à la seconde banque en avance. Pour gérer l'entrelacement, le contrôleur de SDRAM prend en entrée l'adresse envoyée par le processeur, et la découpe en plusieurs champs : un pour sélectionner la banque adéquate, un autre pour la ligne, un autre pour la colonne, un autre pour la rangée. Sans entrelacement, les adresses mémoire sont découpées comme suit : {|class="wikitable" |+ Adresse mémoire |- | Adresse de banque || Adresse de ligne || Adresse de colonne |} Avec l'entrelacement de banques, l'adresse est découpée comme suit. {|class="wikitable" |+ Adresse mémoire |- | Adresse de ligne || Adresse de banque || Adresse de colonne (précise à l'octet) |} Il est aussi possible de faire la même chose, mais avec les rangées ou en utilisant du ''dual channel'', mais je vais passer cela sous silence. Toujours est-il que les méthodes d'entrelacement sont nombreuses et chaque contrôleur mémoire fait un peu à sa sauce. Ils permettent en général de choisir entre plusieurs entrelacements. Au minimum, ils permettent de désactiver l'entrelacement, d'activer un entrelacement de ligne, et d'autres entrelacements plus complexes, dont certains sont propriétaires et tenus secrets par le fabricant. L'entrelacement est géré juste avant le séquenceur mémoire, dans un '''circuit d'entrelacement'''. Ce dernier intervertit certains bits de l'adresse lors des accès mémoires. [[File:Controleur mémoire d'une SDRAM avec entrelacement.png|centre|vignette|upright=2.5|Contrôleur mémoire d'une SDRAM avec entrelacement.]] Les contrôleurs de SDRAM précédents sont assez basiques et ne représentent pas les contrôleurs les plus évolués. Ils étaient utilisés sur les anciens PC, à une époque où ils étaient encore sur la carte mère du processeur. Mais de nos jours, le contrôleur mémoire est intégré au processeur. Et il incorpore de nombreuses optimisations afin de gagner en performances. Malheureusement, ces optimisations sont elles-mêmes dépendantes des optimisations intégrées dans le processeur, ce qui fait qu'on ne peut pas en parler ici. Elles demandent que le contrôleur mémoire reçoive plusieurs accès mémoire simultanés, ce qui n'a aucun sens à ce stade du cours. Aussi, nous allons les passer sous silence pour le moment. Nous les verrons dans le chapitre sur le parallélisme mémoire. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=La désambiguïsation mémoire | prevText=La désambiguïsation mémoire | next=Les processeurs superscalaires in-order | nextText=Les processeurs superscalaires in-order }} </noinclude> bg3qfz00ythf3y32piltwxv4s9f3rrw Fonctionnement d'un ordinateur/La prédiction de branchement 0 65955 773851 773401 2026-10-01T12:50:49Z Mewtow 31375 /* La prédiction de l'adresse de destination pour les branchements indirects et implicites */ 773851 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes avec certaines techniques, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le tau de mauvaises prédictions doit être très faible, de préférence inférieur à 10% dans le meilleur des cas, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, d'une moyenne sur les exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Pour cela, on utilise un '''compteur à saturation''' qui mémorise le nombre de fois qu'un branchement est pris ou non pris. Ce compteur est initialisé de manière à avoir une probabilité proche de 50 %. Le compteur est incrémenté si le branchement est pris et décrémenté s'il est non pris. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs qui utilisent cette technique ont un compteur à saturation par entrée dans le tampon de destination de branchement, donc un compteur à saturation par branchement. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] Rappelons que chaque compteur est associé à une entrée du BTB. Ce qui fait que le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et donc au même compteur à saturation. Cependant, cet ''aliasing'' entraine l'apparition d''''interférences entre branchements''', à savoir que les deux branchements vont agir sur le même compteur à saturation. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents (pour un registre de N bits). Ce registre d'historique est un registre à décalage mis à jour à chaque exécution d'un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction, ce qui tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. Dans les '''unités de prédiction à deux niveaux''', le registre d'historique est combiné avec des compteurs à saturation d'une autre manière. Il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une ou plusieurs '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. En clair, on a un registre d'historique, suivi par une ou plusieurs PHT, ce qui fait que de telles unités de prédiction de branchement sont appelées des '''unités de prédiction de branchement à deux niveaux'''. Il peut y avoir soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment. Sachez juste qu'il est parfaitement possible d'avoir des unités de prédiction avec plusieurs PHTs, ce qui sera utile pour la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] L'unité de prédiction à deux niveaux la plus simple utilise un registre d'historique global, couplé à une PHT unique (une PHT globale). Pour un registre d'historique global unique de n bits, la PHT globale contient 2^n compteurs à saturation, un par valeur possible de l'historique. Pour chaque valeur possible du registre d'historique, la PHT associée contient un compteur à saturation dont la valeur indique si le prochain branchement sera pris ou non-pris. Le choix du compteur à saturation utiliser se fait grâce au registre d'historique global, et éventuellement avec d'autres informations annexes. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Une autre solution pour éliminer l’''aliasing'' est d'utiliser une PHT par branchement. C’est contre-intuitif, car on se dit que l'usage d'un registre d'historique global va avec une PHT unique, mais il y a des contre-exemples où un historique global est associé à plusieurs PHT ! Dans ce cas, chaque PHT est associée à un branchement, ce qui leur vaut le nom de '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] On peut aller plus loin et utiliser non seulement des PHT locales, mais aussi faire quelque chose d'équivalent sur l'historique. Au lieu d'utiliser un historique global, pour tous les branchements, on peut utiliser un historique par branchement. Concrètement, là où les méthodes précédentes mémorisaient le résultat des n derniers branchements, les méthodes qui vont suivre mémorisent, pour chaque branchement dans le BTB, le résultat des n dernières exécutions du branchement. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. L'information mémorisée dans l'historique est alors totalement différente. Il y a un historique par entrée du BTB, soit un historique par branchement connu du processeur. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout élevé. Elles utilisent beaucoup de transistors, consomment beaucoup de courant, chauffent beaucoup, prennent beaucoup de place. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. De plus, les mesures montrent que l'historique global donne de meilleures prédictions que l'historique local, quand on regarde l'ensemble des branchements, mais à condition qu'on utilise des historiques globaux très longs, difficiles à mettre en pratique. Autant dire que les unités de prédiction avec un historique purement local sont rarement utilisées. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ===La prédiction des branchements des boucles=== Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ==La prédiction de branchement à deux niveaux== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Un exemple est celui de l'architecture Silvermont. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L0 et L1'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : une BTB L1 de 512 entrées, et une BTB L2 de 5120 entrées. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs, elle est souvent utilisée pour compléter la BTB de niveau L1. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. De nombreuses unités de prédiction de branchement combinent les résultats de plusieurs unités de prédiction différentes. Mais elles ne vont pas utiliser un historique par unité, mais une unité d'un certain type (compteurs à saturation, historique global, autre) ou des unités spécialisée dans un type de branchements bien précis. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. ===La méta-prédiction=== La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment. ===La fusion de prédictions=== Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée, car elle demande un nombre d'unités important (au moins 4). L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] ===La priorisation des unités de prédiction=== Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> asjzeouvjx5qsgmh1107qbjviky9r7s 773852 773851 2026-10-01T12:51:22Z Mewtow 31375 /* La prédiction de branchement à deux niveaux */ 773852 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes avec certaines techniques, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le tau de mauvaises prédictions doit être très faible, de préférence inférieur à 10% dans le meilleur des cas, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, d'une moyenne sur les exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Pour cela, on utilise un '''compteur à saturation''' qui mémorise le nombre de fois qu'un branchement est pris ou non pris. Ce compteur est initialisé de manière à avoir une probabilité proche de 50 %. Le compteur est incrémenté si le branchement est pris et décrémenté s'il est non pris. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs qui utilisent cette technique ont un compteur à saturation par entrée dans le tampon de destination de branchement, donc un compteur à saturation par branchement. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] Rappelons que chaque compteur est associé à une entrée du BTB. Ce qui fait que le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et donc au même compteur à saturation. Cependant, cet ''aliasing'' entraine l'apparition d''''interférences entre branchements''', à savoir que les deux branchements vont agir sur le même compteur à saturation. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents (pour un registre de N bits). Ce registre d'historique est un registre à décalage mis à jour à chaque exécution d'un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction, ce qui tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. Dans les '''unités de prédiction à deux niveaux''', le registre d'historique est combiné avec des compteurs à saturation d'une autre manière. Il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une ou plusieurs '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. En clair, on a un registre d'historique, suivi par une ou plusieurs PHT, ce qui fait que de telles unités de prédiction de branchement sont appelées des '''unités de prédiction de branchement à deux niveaux'''. Il peut y avoir soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment. Sachez juste qu'il est parfaitement possible d'avoir des unités de prédiction avec plusieurs PHTs, ce qui sera utile pour la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] L'unité de prédiction à deux niveaux la plus simple utilise un registre d'historique global, couplé à une PHT unique (une PHT globale). Pour un registre d'historique global unique de n bits, la PHT globale contient 2^n compteurs à saturation, un par valeur possible de l'historique. Pour chaque valeur possible du registre d'historique, la PHT associée contient un compteur à saturation dont la valeur indique si le prochain branchement sera pris ou non-pris. Le choix du compteur à saturation utiliser se fait grâce au registre d'historique global, et éventuellement avec d'autres informations annexes. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Une autre solution pour éliminer l’''aliasing'' est d'utiliser une PHT par branchement. C’est contre-intuitif, car on se dit que l'usage d'un registre d'historique global va avec une PHT unique, mais il y a des contre-exemples où un historique global est associé à plusieurs PHT ! Dans ce cas, chaque PHT est associée à un branchement, ce qui leur vaut le nom de '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] On peut aller plus loin et utiliser non seulement des PHT locales, mais aussi faire quelque chose d'équivalent sur l'historique. Au lieu d'utiliser un historique global, pour tous les branchements, on peut utiliser un historique par branchement. Concrètement, là où les méthodes précédentes mémorisaient le résultat des n derniers branchements, les méthodes qui vont suivre mémorisent, pour chaque branchement dans le BTB, le résultat des n dernières exécutions du branchement. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. L'information mémorisée dans l'historique est alors totalement différente. Il y a un historique par entrée du BTB, soit un historique par branchement connu du processeur. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout élevé. Elles utilisent beaucoup de transistors, consomment beaucoup de courant, chauffent beaucoup, prennent beaucoup de place. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. De plus, les mesures montrent que l'historique global donne de meilleures prédictions que l'historique local, quand on regarde l'ensemble des branchements, mais à condition qu'on utilise des historiques globaux très longs, difficiles à mettre en pratique. Autant dire que les unités de prédiction avec un historique purement local sont rarement utilisées. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ===La prédiction des branchements des boucles=== Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ==La prédiction de branchement à deux niveaux== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. De nombreuses unités de prédiction de branchement combinent les résultats de plusieurs unités de prédiction différentes. Mais elles ne vont pas utiliser un historique par unité, mais une unité d'un certain type (compteurs à saturation, historique global, autre) ou des unités spécialisée dans un type de branchements bien précis. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. ===La méta-prédiction=== La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment. ===La fusion de prédictions=== Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée, car elle demande un nombre d'unités important (au moins 4). L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] ===La priorisation des unités de prédiction=== Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> 3hftgzmwlj2wpuv96kgpwbex9otvxn8 773853 773852 2026-10-01T13:08:32Z Mewtow 31375 /* La prédiction de l'adresse de retour d'une fonction */ 773853 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapotre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits à au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction, etc. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie si celle-ci est une instruction de retour de fonction et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait qles instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes avec certaines techniques, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le tau de mauvaises prédictions doit être très faible, de préférence inférieur à 10% dans le meilleur des cas, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, d'une moyenne sur les exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Pour cela, on utilise un '''compteur à saturation''' qui mémorise le nombre de fois qu'un branchement est pris ou non pris. Ce compteur est initialisé de manière à avoir une probabilité proche de 50 %. Le compteur est incrémenté si le branchement est pris et décrémenté s'il est non pris. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs qui utilisent cette technique ont un compteur à saturation par entrée dans le tampon de destination de branchement, donc un compteur à saturation par branchement. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] Rappelons que chaque compteur est associé à une entrée du BTB. Ce qui fait que le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et donc au même compteur à saturation. Cependant, cet ''aliasing'' entraine l'apparition d''''interférences entre branchements''', à savoir que les deux branchements vont agir sur le même compteur à saturation. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents (pour un registre de N bits). Ce registre d'historique est un registre à décalage mis à jour à chaque exécution d'un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction, ce qui tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. Dans les '''unités de prédiction à deux niveaux''', le registre d'historique est combiné avec des compteurs à saturation d'une autre manière. Il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une ou plusieurs '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. En clair, on a un registre d'historique, suivi par une ou plusieurs PHT, ce qui fait que de telles unités de prédiction de branchement sont appelées des '''unités de prédiction de branchement à deux niveaux'''. Il peut y avoir soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment. Sachez juste qu'il est parfaitement possible d'avoir des unités de prédiction avec plusieurs PHTs, ce qui sera utile pour la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] L'unité de prédiction à deux niveaux la plus simple utilise un registre d'historique global, couplé à une PHT unique (une PHT globale). Pour un registre d'historique global unique de n bits, la PHT globale contient 2^n compteurs à saturation, un par valeur possible de l'historique. Pour chaque valeur possible du registre d'historique, la PHT associée contient un compteur à saturation dont la valeur indique si le prochain branchement sera pris ou non-pris. Le choix du compteur à saturation utiliser se fait grâce au registre d'historique global, et éventuellement avec d'autres informations annexes. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Une autre solution pour éliminer l’''aliasing'' est d'utiliser une PHT par branchement. C’est contre-intuitif, car on se dit que l'usage d'un registre d'historique global va avec une PHT unique, mais il y a des contre-exemples où un historique global est associé à plusieurs PHT ! Dans ce cas, chaque PHT est associée à un branchement, ce qui leur vaut le nom de '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] On peut aller plus loin et utiliser non seulement des PHT locales, mais aussi faire quelque chose d'équivalent sur l'historique. Au lieu d'utiliser un historique global, pour tous les branchements, on peut utiliser un historique par branchement. Concrètement, là où les méthodes précédentes mémorisaient le résultat des n derniers branchements, les méthodes qui vont suivre mémorisent, pour chaque branchement dans le BTB, le résultat des n dernières exécutions du branchement. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. L'information mémorisée dans l'historique est alors totalement différente. Il y a un historique par entrée du BTB, soit un historique par branchement connu du processeur. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout élevé. Elles utilisent beaucoup de transistors, consomment beaucoup de courant, chauffent beaucoup, prennent beaucoup de place. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. De plus, les mesures montrent que l'historique global donne de meilleures prédictions que l'historique local, quand on regarde l'ensemble des branchements, mais à condition qu'on utilise des historiques globaux très longs, difficiles à mettre en pratique. Autant dire que les unités de prédiction avec un historique purement local sont rarement utilisées. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ===La prédiction des branchements des boucles=== Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ==La prédiction de branchement à deux niveaux== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. De nombreuses unités de prédiction de branchement combinent les résultats de plusieurs unités de prédiction différentes. Mais elles ne vont pas utiliser un historique par unité, mais une unité d'un certain type (compteurs à saturation, historique global, autre) ou des unités spécialisée dans un type de branchements bien précis. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. ===La méta-prédiction=== La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment. ===La fusion de prédictions=== Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée, car elle demande un nombre d'unités important (au moins 4). L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] ===La priorisation des unités de prédiction=== Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> gx62h3rqctl1cfwx7tn7zs7d4oelyun 773863 773853 2026-10-01T14:20:27Z Mewtow 31375 /* La prédiction de l'adresse de retour d'une fonction */ 773863 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes avec certaines techniques, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le tau de mauvaises prédictions doit être très faible, de préférence inférieur à 10% dans le meilleur des cas, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, d'une moyenne sur les exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Pour cela, on utilise un '''compteur à saturation''' qui mémorise le nombre de fois qu'un branchement est pris ou non pris. Ce compteur est initialisé de manière à avoir une probabilité proche de 50 %. Le compteur est incrémenté si le branchement est pris et décrémenté s'il est non pris. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs qui utilisent cette technique ont un compteur à saturation par entrée dans le tampon de destination de branchement, donc un compteur à saturation par branchement. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] Rappelons que chaque compteur est associé à une entrée du BTB. Ce qui fait que le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et donc au même compteur à saturation. Cependant, cet ''aliasing'' entraine l'apparition d''''interférences entre branchements''', à savoir que les deux branchements vont agir sur le même compteur à saturation. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents (pour un registre de N bits). Ce registre d'historique est un registre à décalage mis à jour à chaque exécution d'un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction, ce qui tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. Dans les '''unités de prédiction à deux niveaux''', le registre d'historique est combiné avec des compteurs à saturation d'une autre manière. Il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une ou plusieurs '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. En clair, on a un registre d'historique, suivi par une ou plusieurs PHT, ce qui fait que de telles unités de prédiction de branchement sont appelées des '''unités de prédiction de branchement à deux niveaux'''. Il peut y avoir soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment. Sachez juste qu'il est parfaitement possible d'avoir des unités de prédiction avec plusieurs PHTs, ce qui sera utile pour la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] L'unité de prédiction à deux niveaux la plus simple utilise un registre d'historique global, couplé à une PHT unique (une PHT globale). Pour un registre d'historique global unique de n bits, la PHT globale contient 2^n compteurs à saturation, un par valeur possible de l'historique. Pour chaque valeur possible du registre d'historique, la PHT associée contient un compteur à saturation dont la valeur indique si le prochain branchement sera pris ou non-pris. Le choix du compteur à saturation utiliser se fait grâce au registre d'historique global, et éventuellement avec d'autres informations annexes. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Une autre solution pour éliminer l’''aliasing'' est d'utiliser une PHT par branchement. C’est contre-intuitif, car on se dit que l'usage d'un registre d'historique global va avec une PHT unique, mais il y a des contre-exemples où un historique global est associé à plusieurs PHT ! Dans ce cas, chaque PHT est associée à un branchement, ce qui leur vaut le nom de '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] On peut aller plus loin et utiliser non seulement des PHT locales, mais aussi faire quelque chose d'équivalent sur l'historique. Au lieu d'utiliser un historique global, pour tous les branchements, on peut utiliser un historique par branchement. Concrètement, là où les méthodes précédentes mémorisaient le résultat des n derniers branchements, les méthodes qui vont suivre mémorisent, pour chaque branchement dans le BTB, le résultat des n dernières exécutions du branchement. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. L'information mémorisée dans l'historique est alors totalement différente. Il y a un historique par entrée du BTB, soit un historique par branchement connu du processeur. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout élevé. Elles utilisent beaucoup de transistors, consomment beaucoup de courant, chauffent beaucoup, prennent beaucoup de place. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. De plus, les mesures montrent que l'historique global donne de meilleures prédictions que l'historique local, quand on regarde l'ensemble des branchements, mais à condition qu'on utilise des historiques globaux très longs, difficiles à mettre en pratique. Autant dire que les unités de prédiction avec un historique purement local sont rarement utilisées. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ===La prédiction des branchements des boucles=== Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ==La prédiction de branchement à deux niveaux== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. De nombreuses unités de prédiction de branchement combinent les résultats de plusieurs unités de prédiction différentes. Mais elles ne vont pas utiliser un historique par unité, mais une unité d'un certain type (compteurs à saturation, historique global, autre) ou des unités spécialisée dans un type de branchements bien précis. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. ===La méta-prédiction=== La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment. ===La fusion de prédictions=== Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée, car elle demande un nombre d'unités important (au moins 4). L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] ===La priorisation des unités de prédiction=== Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> f12d8ce4qht2enfg5ns7b7byll53pjz 773864 773863 2026-10-01T14:21:39Z Mewtow 31375 /* Les contraintes d’implémentation de la prédiction de branchement */ 773864 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, d'une moyenne sur les exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Pour cela, on utilise un '''compteur à saturation''' qui mémorise le nombre de fois qu'un branchement est pris ou non pris. Ce compteur est initialisé de manière à avoir une probabilité proche de 50 %. Le compteur est incrémenté si le branchement est pris et décrémenté s'il est non pris. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs qui utilisent cette technique ont un compteur à saturation par entrée dans le tampon de destination de branchement, donc un compteur à saturation par branchement. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] Rappelons que chaque compteur est associé à une entrée du BTB. Ce qui fait que le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et donc au même compteur à saturation. Cependant, cet ''aliasing'' entraine l'apparition d''''interférences entre branchements''', à savoir que les deux branchements vont agir sur le même compteur à saturation. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents (pour un registre de N bits). Ce registre d'historique est un registre à décalage mis à jour à chaque exécution d'un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction, ce qui tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. Dans les '''unités de prédiction à deux niveaux''', le registre d'historique est combiné avec des compteurs à saturation d'une autre manière. Il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une ou plusieurs '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. En clair, on a un registre d'historique, suivi par une ou plusieurs PHT, ce qui fait que de telles unités de prédiction de branchement sont appelées des '''unités de prédiction de branchement à deux niveaux'''. Il peut y avoir soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment. Sachez juste qu'il est parfaitement possible d'avoir des unités de prédiction avec plusieurs PHTs, ce qui sera utile pour la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] L'unité de prédiction à deux niveaux la plus simple utilise un registre d'historique global, couplé à une PHT unique (une PHT globale). Pour un registre d'historique global unique de n bits, la PHT globale contient 2^n compteurs à saturation, un par valeur possible de l'historique. Pour chaque valeur possible du registre d'historique, la PHT associée contient un compteur à saturation dont la valeur indique si le prochain branchement sera pris ou non-pris. Le choix du compteur à saturation utiliser se fait grâce au registre d'historique global, et éventuellement avec d'autres informations annexes. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Une autre solution pour éliminer l’''aliasing'' est d'utiliser une PHT par branchement. C’est contre-intuitif, car on se dit que l'usage d'un registre d'historique global va avec une PHT unique, mais il y a des contre-exemples où un historique global est associé à plusieurs PHT ! Dans ce cas, chaque PHT est associée à un branchement, ce qui leur vaut le nom de '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] On peut aller plus loin et utiliser non seulement des PHT locales, mais aussi faire quelque chose d'équivalent sur l'historique. Au lieu d'utiliser un historique global, pour tous les branchements, on peut utiliser un historique par branchement. Concrètement, là où les méthodes précédentes mémorisaient le résultat des n derniers branchements, les méthodes qui vont suivre mémorisent, pour chaque branchement dans le BTB, le résultat des n dernières exécutions du branchement. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. L'information mémorisée dans l'historique est alors totalement différente. Il y a un historique par entrée du BTB, soit un historique par branchement connu du processeur. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout élevé. Elles utilisent beaucoup de transistors, consomment beaucoup de courant, chauffent beaucoup, prennent beaucoup de place. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. De plus, les mesures montrent que l'historique global donne de meilleures prédictions que l'historique local, quand on regarde l'ensemble des branchements, mais à condition qu'on utilise des historiques globaux très longs, difficiles à mettre en pratique. Autant dire que les unités de prédiction avec un historique purement local sont rarement utilisées. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ===La prédiction des branchements des boucles=== Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ==La prédiction de branchement à deux niveaux== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. De nombreuses unités de prédiction de branchement combinent les résultats de plusieurs unités de prédiction différentes. Mais elles ne vont pas utiliser un historique par unité, mais une unité d'un certain type (compteurs à saturation, historique global, autre) ou des unités spécialisée dans un type de branchements bien précis. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. ===La méta-prédiction=== La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment. ===La fusion de prédictions=== Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée, car elle demande un nombre d'unités important (au moins 4). L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] ===La priorisation des unités de prédiction=== Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> crje5q3xnv4xoyr3t96wg4ckfsag8lv 773865 773864 2026-10-01T14:22:00Z Mewtow 31375 /* La classification des techniques de prédiction de branchement */ 773865 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, d'une moyenne sur les exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Pour cela, on utilise un '''compteur à saturation''' qui mémorise le nombre de fois qu'un branchement est pris ou non pris. Ce compteur est initialisé de manière à avoir une probabilité proche de 50 %. Le compteur est incrémenté si le branchement est pris et décrémenté s'il est non pris. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs qui utilisent cette technique ont un compteur à saturation par entrée dans le tampon de destination de branchement, donc un compteur à saturation par branchement. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] Rappelons que chaque compteur est associé à une entrée du BTB. Ce qui fait que le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et donc au même compteur à saturation. Cependant, cet ''aliasing'' entraine l'apparition d''''interférences entre branchements''', à savoir que les deux branchements vont agir sur le même compteur à saturation. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents (pour un registre de N bits). Ce registre d'historique est un registre à décalage mis à jour à chaque exécution d'un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction, ce qui tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. Dans les '''unités de prédiction à deux niveaux''', le registre d'historique est combiné avec des compteurs à saturation d'une autre manière. Il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une ou plusieurs '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. En clair, on a un registre d'historique, suivi par une ou plusieurs PHT, ce qui fait que de telles unités de prédiction de branchement sont appelées des '''unités de prédiction de branchement à deux niveaux'''. Il peut y avoir soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment. Sachez juste qu'il est parfaitement possible d'avoir des unités de prédiction avec plusieurs PHTs, ce qui sera utile pour la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] L'unité de prédiction à deux niveaux la plus simple utilise un registre d'historique global, couplé à une PHT unique (une PHT globale). Pour un registre d'historique global unique de n bits, la PHT globale contient 2^n compteurs à saturation, un par valeur possible de l'historique. Pour chaque valeur possible du registre d'historique, la PHT associée contient un compteur à saturation dont la valeur indique si le prochain branchement sera pris ou non-pris. Le choix du compteur à saturation utiliser se fait grâce au registre d'historique global, et éventuellement avec d'autres informations annexes. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Une autre solution pour éliminer l’''aliasing'' est d'utiliser une PHT par branchement. C’est contre-intuitif, car on se dit que l'usage d'un registre d'historique global va avec une PHT unique, mais il y a des contre-exemples où un historique global est associé à plusieurs PHT ! Dans ce cas, chaque PHT est associée à un branchement, ce qui leur vaut le nom de '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] On peut aller plus loin et utiliser non seulement des PHT locales, mais aussi faire quelque chose d'équivalent sur l'historique. Au lieu d'utiliser un historique global, pour tous les branchements, on peut utiliser un historique par branchement. Concrètement, là où les méthodes précédentes mémorisaient le résultat des n derniers branchements, les méthodes qui vont suivre mémorisent, pour chaque branchement dans le BTB, le résultat des n dernières exécutions du branchement. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. L'information mémorisée dans l'historique est alors totalement différente. Il y a un historique par entrée du BTB, soit un historique par branchement connu du processeur. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout élevé. Elles utilisent beaucoup de transistors, consomment beaucoup de courant, chauffent beaucoup, prennent beaucoup de place. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. De plus, les mesures montrent que l'historique global donne de meilleures prédictions que l'historique local, quand on regarde l'ensemble des branchements, mais à condition qu'on utilise des historiques globaux très longs, difficiles à mettre en pratique. Autant dire que les unités de prédiction avec un historique purement local sont rarement utilisées. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ===La prédiction des branchements des boucles=== Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ==La prédiction de branchement à deux niveaux== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. De nombreuses unités de prédiction de branchement combinent les résultats de plusieurs unités de prédiction différentes. Mais elles ne vont pas utiliser un historique par unité, mais une unité d'un certain type (compteurs à saturation, historique global, autre) ou des unités spécialisée dans un type de branchements bien précis. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. ===La méta-prédiction=== La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment. ===La fusion de prédictions=== Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée, car elle demande un nombre d'unités important (au moins 4). L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] ===La priorisation des unités de prédiction=== Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> 1zyza23lc4yhd8vjv22rgh38r8azhxh 773866 773865 2026-10-01T14:22:24Z Mewtow 31375 /* La prédiction statique de branchement */ 773866 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, d'une moyenne sur les exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Pour cela, on utilise un '''compteur à saturation''' qui mémorise le nombre de fois qu'un branchement est pris ou non pris. Ce compteur est initialisé de manière à avoir une probabilité proche de 50 %. Le compteur est incrémenté si le branchement est pris et décrémenté s'il est non pris. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs qui utilisent cette technique ont un compteur à saturation par entrée dans le tampon de destination de branchement, donc un compteur à saturation par branchement. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] Rappelons que chaque compteur est associé à une entrée du BTB. Ce qui fait que le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et donc au même compteur à saturation. Cependant, cet ''aliasing'' entraine l'apparition d''''interférences entre branchements''', à savoir que les deux branchements vont agir sur le même compteur à saturation. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents (pour un registre de N bits). Ce registre d'historique est un registre à décalage mis à jour à chaque exécution d'un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction, ce qui tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. Dans les '''unités de prédiction à deux niveaux''', le registre d'historique est combiné avec des compteurs à saturation d'une autre manière. Il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une ou plusieurs '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. En clair, on a un registre d'historique, suivi par une ou plusieurs PHT, ce qui fait que de telles unités de prédiction de branchement sont appelées des '''unités de prédiction de branchement à deux niveaux'''. Il peut y avoir soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment. Sachez juste qu'il est parfaitement possible d'avoir des unités de prédiction avec plusieurs PHTs, ce qui sera utile pour la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] L'unité de prédiction à deux niveaux la plus simple utilise un registre d'historique global, couplé à une PHT unique (une PHT globale). Pour un registre d'historique global unique de n bits, la PHT globale contient 2^n compteurs à saturation, un par valeur possible de l'historique. Pour chaque valeur possible du registre d'historique, la PHT associée contient un compteur à saturation dont la valeur indique si le prochain branchement sera pris ou non-pris. Le choix du compteur à saturation utiliser se fait grâce au registre d'historique global, et éventuellement avec d'autres informations annexes. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Une autre solution pour éliminer l’''aliasing'' est d'utiliser une PHT par branchement. C’est contre-intuitif, car on se dit que l'usage d'un registre d'historique global va avec une PHT unique, mais il y a des contre-exemples où un historique global est associé à plusieurs PHT ! Dans ce cas, chaque PHT est associée à un branchement, ce qui leur vaut le nom de '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] On peut aller plus loin et utiliser non seulement des PHT locales, mais aussi faire quelque chose d'équivalent sur l'historique. Au lieu d'utiliser un historique global, pour tous les branchements, on peut utiliser un historique par branchement. Concrètement, là où les méthodes précédentes mémorisaient le résultat des n derniers branchements, les méthodes qui vont suivre mémorisent, pour chaque branchement dans le BTB, le résultat des n dernières exécutions du branchement. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. L'information mémorisée dans l'historique est alors totalement différente. Il y a un historique par entrée du BTB, soit un historique par branchement connu du processeur. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout élevé. Elles utilisent beaucoup de transistors, consomment beaucoup de courant, chauffent beaucoup, prennent beaucoup de place. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. De plus, les mesures montrent que l'historique global donne de meilleures prédictions que l'historique local, quand on regarde l'ensemble des branchements, mais à condition qu'on utilise des historiques globaux très longs, difficiles à mettre en pratique. Autant dire que les unités de prédiction avec un historique purement local sont rarement utilisées. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ===La prédiction des branchements des boucles=== Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ==La prédiction de branchement à deux niveaux== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. De nombreuses unités de prédiction de branchement combinent les résultats de plusieurs unités de prédiction différentes. Mais elles ne vont pas utiliser un historique par unité, mais une unité d'un certain type (compteurs à saturation, historique global, autre) ou des unités spécialisée dans un type de branchements bien précis. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. ===La méta-prédiction=== La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment. ===La fusion de prédictions=== Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée, car elle demande un nombre d'unités important (au moins 4). L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] ===La priorisation des unités de prédiction=== Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> dzdryvaa10fwy2wyjjlcb9yu0d2gaim 773867 773866 2026-10-01T14:30:46Z Mewtow 31375 /* La prédiction de branchement avec des compteurs à saturation */ 773867 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents (pour un registre de N bits). Ce registre d'historique est un registre à décalage mis à jour à chaque exécution d'un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction, ce qui tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. Dans les '''unités de prédiction à deux niveaux''', le registre d'historique est combiné avec des compteurs à saturation d'une autre manière. Il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une ou plusieurs '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. En clair, on a un registre d'historique, suivi par une ou plusieurs PHT, ce qui fait que de telles unités de prédiction de branchement sont appelées des '''unités de prédiction de branchement à deux niveaux'''. Il peut y avoir soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment. Sachez juste qu'il est parfaitement possible d'avoir des unités de prédiction avec plusieurs PHTs, ce qui sera utile pour la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] L'unité de prédiction à deux niveaux la plus simple utilise un registre d'historique global, couplé à une PHT unique (une PHT globale). Pour un registre d'historique global unique de n bits, la PHT globale contient 2^n compteurs à saturation, un par valeur possible de l'historique. Pour chaque valeur possible du registre d'historique, la PHT associée contient un compteur à saturation dont la valeur indique si le prochain branchement sera pris ou non-pris. Le choix du compteur à saturation utiliser se fait grâce au registre d'historique global, et éventuellement avec d'autres informations annexes. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Une autre solution pour éliminer l’''aliasing'' est d'utiliser une PHT par branchement. C’est contre-intuitif, car on se dit que l'usage d'un registre d'historique global va avec une PHT unique, mais il y a des contre-exemples où un historique global est associé à plusieurs PHT ! Dans ce cas, chaque PHT est associée à un branchement, ce qui leur vaut le nom de '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] On peut aller plus loin et utiliser non seulement des PHT locales, mais aussi faire quelque chose d'équivalent sur l'historique. Au lieu d'utiliser un historique global, pour tous les branchements, on peut utiliser un historique par branchement. Concrètement, là où les méthodes précédentes mémorisaient le résultat des n derniers branchements, les méthodes qui vont suivre mémorisent, pour chaque branchement dans le BTB, le résultat des n dernières exécutions du branchement. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. L'information mémorisée dans l'historique est alors totalement différente. Il y a un historique par entrée du BTB, soit un historique par branchement connu du processeur. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout élevé. Elles utilisent beaucoup de transistors, consomment beaucoup de courant, chauffent beaucoup, prennent beaucoup de place. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. De plus, les mesures montrent que l'historique global donne de meilleures prédictions que l'historique local, quand on regarde l'ensemble des branchements, mais à condition qu'on utilise des historiques globaux très longs, difficiles à mettre en pratique. Autant dire que les unités de prédiction avec un historique purement local sont rarement utilisées. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ===La prédiction des branchements des boucles=== Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ==La prédiction de branchement à deux niveaux== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. De nombreuses unités de prédiction de branchement combinent les résultats de plusieurs unités de prédiction différentes. Mais elles ne vont pas utiliser un historique par unité, mais une unité d'un certain type (compteurs à saturation, historique global, autre) ou des unités spécialisée dans un type de branchements bien précis. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. ===La méta-prédiction=== La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment. ===La fusion de prédictions=== Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée, car elle demande un nombre d'unités important (au moins 4). L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] ===La priorisation des unités de prédiction=== Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> d7eavbu1mxuew3g2eohobav70f1610r 773868 773867 2026-10-01T14:38:12Z Mewtow 31375 /* La prédiction de branchement à deux niveaux avec un historique global */ 773868 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents (pour un registre de N bits). Ce registre d'historique est un registre à décalage mis à jour à chaque exécution d'un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction, ce qui tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. Dans les '''unités de prédiction à deux niveaux''', le registre d'historique est combiné avec des compteurs à saturation d'une autre manière. Il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une ou plusieurs '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. En clair, on a un registre d'historique, suivi par une ou plusieurs PHT, ce qui fait que de telles unités de prédiction de branchement sont appelées des '''unités de prédiction de branchement à deux niveaux'''. Il peut y avoir soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment. Sachez juste qu'il est parfaitement possible d'avoir des unités de prédiction avec plusieurs PHTs, ce qui sera utile pour la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] L'unité de prédiction à deux niveaux la plus simple utilise un registre d'historique global, couplé à une PHT unique (une PHT globale). Pour un registre d'historique global unique de n bits, la PHT globale contient 2^n compteurs à saturation, un par valeur possible de l'historique. Pour chaque valeur possible du registre d'historique, la PHT associée contient un compteur à saturation dont la valeur indique si le prochain branchement sera pris ou non-pris. Le choix du compteur à saturation utiliser se fait grâce au registre d'historique global, et éventuellement avec d'autres informations annexes. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Une autre solution pour éliminer l’''aliasing'' est d'utiliser une PHT par branchement. C’est contre-intuitif, car on se dit que l'usage d'un registre d'historique global va avec une PHT unique, mais il y a des contre-exemples où un historique global est associé à plusieurs PHT ! Dans ce cas, chaque PHT est associée à un branchement, ce qui leur vaut le nom de '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] On peut aller plus loin et utiliser non seulement des PHT locales, mais aussi faire quelque chose d'équivalent sur l'historique. Au lieu d'utiliser un historique global, pour tous les branchements, on peut utiliser un historique par branchement. Concrètement, là où les méthodes précédentes mémorisaient le résultat des n derniers branchements, les méthodes qui vont suivre mémorisent, pour chaque branchement dans le BTB, le résultat des n dernières exécutions du branchement. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. L'information mémorisée dans l'historique est alors totalement différente. Il y a un historique par entrée du BTB, soit un historique par branchement connu du processeur. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout élevé. Elles utilisent beaucoup de transistors, consomment beaucoup de courant, chauffent beaucoup, prennent beaucoup de place. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. De plus, les mesures montrent que l'historique global donne de meilleures prédictions que l'historique local, quand on regarde l'ensemble des branchements, mais à condition qu'on utilise des historiques globaux très longs, difficiles à mettre en pratique. Autant dire que les unités de prédiction avec un historique purement local sont rarement utilisées. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ===La prédiction des branchements des boucles=== Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ==La prédiction de branchement à deux niveaux== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. De nombreuses unités de prédiction de branchement combinent les résultats de plusieurs unités de prédiction différentes. Mais elles ne vont pas utiliser un historique par unité, mais une unité d'un certain type (compteurs à saturation, historique global, autre) ou des unités spécialisée dans un type de branchements bien précis. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. ===La méta-prédiction=== La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment. ===La fusion de prédictions=== Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée, car elle demande un nombre d'unités important (au moins 4). L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] ===La priorisation des unités de prédiction=== Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> fyle7drz3mgn0nfo9opq3n1lxw5ghx3 773869 773868 2026-10-01T14:48:27Z Mewtow 31375 /* La prédiction de branchement à deux niveaux avec un historique global */ 773869 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents. Ce registre d'historique est un registre à décalage mis à jour à chaque fois que le processeur exécute un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction. L'unité de prédiction globale utilise un compteur à saturation pour chaque valeur possible du registre d'historique. En clair, pour un registre de N bits, elle utilise 2^n compteurs à saturation. En clair, il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Avec un registre d'historique, la prédiction tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. L'unité de prédiction précédente est très simple et il existe des unités de prédiction similaires, basés sur des principes quelque peu différentes. Elles sont appelées des '''unités de prédiction à deux niveaux''', ce qui sous-entend que le registre d'historique et la PHT sont deux niveaux différents. Elles peuvent utiliser soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment, les unités de prédiction avec plusieurs PHTs seront vues dans la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] Dans l'unité de prédiction précédente, le choix du compteur à saturation utiliser se fait uniquement grâce au registre d'historique global. les unités que nous allons voir font un choix plus complexe, qui utilisent d'autres informations. Mais pourquoi utiliser d'autres informations ? Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Une autre solution pour éliminer l’''aliasing'' est d'utiliser une PHT par branchement. C’est contre-intuitif, car on se dit que l'usage d'un registre d'historique global va avec une PHT unique, mais il y a des contre-exemples où un historique global est associé à plusieurs PHT ! Dans ce cas, chaque PHT est associée à un branchement, ce qui leur vaut le nom de '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] On peut aller plus loin et utiliser non seulement des PHT locales, mais aussi faire quelque chose d'équivalent sur l'historique. Au lieu d'utiliser un historique global, pour tous les branchements, on peut utiliser un historique par branchement. Concrètement, là où les méthodes précédentes mémorisaient le résultat des n derniers branchements, les méthodes qui vont suivre mémorisent, pour chaque branchement dans le BTB, le résultat des n dernières exécutions du branchement. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. L'information mémorisée dans l'historique est alors totalement différente. Il y a un historique par entrée du BTB, soit un historique par branchement connu du processeur. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout élevé. Elles utilisent beaucoup de transistors, consomment beaucoup de courant, chauffent beaucoup, prennent beaucoup de place. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. De plus, les mesures montrent que l'historique global donne de meilleures prédictions que l'historique local, quand on regarde l'ensemble des branchements, mais à condition qu'on utilise des historiques globaux très longs, difficiles à mettre en pratique. Autant dire que les unités de prédiction avec un historique purement local sont rarement utilisées. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ===La prédiction des branchements des boucles=== Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ==La prédiction de branchement à deux niveaux== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. De nombreuses unités de prédiction de branchement combinent les résultats de plusieurs unités de prédiction différentes. Mais elles ne vont pas utiliser un historique par unité, mais une unité d'un certain type (compteurs à saturation, historique global, autre) ou des unités spécialisée dans un type de branchements bien précis. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. ===La méta-prédiction=== La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment. ===La fusion de prédictions=== Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée, car elle demande un nombre d'unités important (au moins 4). L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] ===La priorisation des unités de prédiction=== Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> s64i73r5uj9b5x2t871wvcs8jrtl2rq 773870 773869 2026-10-01T14:54:10Z Mewtow 31375 /* La prédiction avec plusieurs unités de branchement distinctes */ 773870 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents. Ce registre d'historique est un registre à décalage mis à jour à chaque fois que le processeur exécute un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction. L'unité de prédiction globale utilise un compteur à saturation pour chaque valeur possible du registre d'historique. En clair, pour un registre de N bits, elle utilise 2^n compteurs à saturation. En clair, il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Avec un registre d'historique, la prédiction tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. L'unité de prédiction précédente est très simple et il existe des unités de prédiction similaires, basés sur des principes quelque peu différentes. Elles sont appelées des '''unités de prédiction à deux niveaux''', ce qui sous-entend que le registre d'historique et la PHT sont deux niveaux différents. Elles peuvent utiliser soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment, les unités de prédiction avec plusieurs PHTs seront vues dans la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] Dans l'unité de prédiction précédente, le choix du compteur à saturation utiliser se fait uniquement grâce au registre d'historique global. les unités que nous allons voir font un choix plus complexe, qui utilisent d'autres informations. Mais pourquoi utiliser d'autres informations ? Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Une autre solution pour éliminer l’''aliasing'' est d'utiliser une PHT par branchement. C’est contre-intuitif, car on se dit que l'usage d'un registre d'historique global va avec une PHT unique, mais il y a des contre-exemples où un historique global est associé à plusieurs PHT ! Dans ce cas, chaque PHT est associée à un branchement, ce qui leur vaut le nom de '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] On peut aller plus loin et utiliser non seulement des PHT locales, mais aussi faire quelque chose d'équivalent sur l'historique. Au lieu d'utiliser un historique global, pour tous les branchements, on peut utiliser un historique par branchement. Concrètement, là où les méthodes précédentes mémorisaient le résultat des n derniers branchements, les méthodes qui vont suivre mémorisent, pour chaque branchement dans le BTB, le résultat des n dernières exécutions du branchement. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. L'information mémorisée dans l'historique est alors totalement différente. Il y a un historique par entrée du BTB, soit un historique par branchement connu du processeur. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout élevé. Elles utilisent beaucoup de transistors, consomment beaucoup de courant, chauffent beaucoup, prennent beaucoup de place. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. De plus, les mesures montrent que l'historique global donne de meilleures prédictions que l'historique local, quand on regarde l'ensemble des branchements, mais à condition qu'on utilise des historiques globaux très longs, difficiles à mettre en pratique. Autant dire que les unités de prédiction avec un historique purement local sont rarement utilisées. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ===La prédiction des branchements des boucles=== Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ==La prédiction de branchement à deux niveaux== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. ===Le prédicteur L-TAGE=== Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Une méta-unité de branchement prédit alors quelle taille d'historique donne les meilleurs résultats. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. C'est d'ailleurs le principe derrière les '''unités de branchement TAGE (''TAgged GEometric length predictor'')'''. Elles utilisent plusieurs registres d'historiques, chacun avec sa propre PHT, dont la taille suit une suite géométrique. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. Les unités de branchement TAGE sont les plus utilisées au moment où j'écris ces lignes (2026), et la majorité des processeurs x86 commerciaux utilisent une variante de ce prédicteur, à quelques exceptions près. ===La méta-prédiction=== La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment. ===La fusion de prédictions=== Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée, car elle demande un nombre d'unités important (au moins 4). L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] ===La priorisation des unités de prédiction=== Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> l8u4fxnrrjh6p44gd3mje7pa8lxts4k 773871 773870 2026-10-01T14:57:31Z Mewtow 31375 /* Le prédicteur L-TAGE */ 773871 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents. Ce registre d'historique est un registre à décalage mis à jour à chaque fois que le processeur exécute un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction. L'unité de prédiction globale utilise un compteur à saturation pour chaque valeur possible du registre d'historique. En clair, pour un registre de N bits, elle utilise 2^n compteurs à saturation. En clair, il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Avec un registre d'historique, la prédiction tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. L'unité de prédiction précédente est très simple et il existe des unités de prédiction similaires, basés sur des principes quelque peu différentes. Elles sont appelées des '''unités de prédiction à deux niveaux''', ce qui sous-entend que le registre d'historique et la PHT sont deux niveaux différents. Elles peuvent utiliser soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment, les unités de prédiction avec plusieurs PHTs seront vues dans la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] Dans l'unité de prédiction précédente, le choix du compteur à saturation utiliser se fait uniquement grâce au registre d'historique global. les unités que nous allons voir font un choix plus complexe, qui utilisent d'autres informations. Mais pourquoi utiliser d'autres informations ? Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Une autre solution pour éliminer l’''aliasing'' est d'utiliser une PHT par branchement. C’est contre-intuitif, car on se dit que l'usage d'un registre d'historique global va avec une PHT unique, mais il y a des contre-exemples où un historique global est associé à plusieurs PHT ! Dans ce cas, chaque PHT est associée à un branchement, ce qui leur vaut le nom de '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] On peut aller plus loin et utiliser non seulement des PHT locales, mais aussi faire quelque chose d'équivalent sur l'historique. Au lieu d'utiliser un historique global, pour tous les branchements, on peut utiliser un historique par branchement. Concrètement, là où les méthodes précédentes mémorisaient le résultat des n derniers branchements, les méthodes qui vont suivre mémorisent, pour chaque branchement dans le BTB, le résultat des n dernières exécutions du branchement. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. L'information mémorisée dans l'historique est alors totalement différente. Il y a un historique par entrée du BTB, soit un historique par branchement connu du processeur. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout élevé. Elles utilisent beaucoup de transistors, consomment beaucoup de courant, chauffent beaucoup, prennent beaucoup de place. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. De plus, les mesures montrent que l'historique global donne de meilleures prédictions que l'historique local, quand on regarde l'ensemble des branchements, mais à condition qu'on utilise des historiques globaux très longs, difficiles à mettre en pratique. Autant dire que les unités de prédiction avec un historique purement local sont rarement utilisées. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ===La prédiction des branchements des boucles=== Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ==La prédiction de branchement à deux niveaux== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. ===Le prédicteur L-TAGE=== Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Une méta-unité de branchement prédit alors quelle taille d'historique donne les meilleurs résultats. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. C'est d'ailleurs le principe derrière les '''unités de branchement TAGE (''TAgged GEometric length predictor'')'''. Elles utilisent plusieurs registres d'historiques, chacun avec sa propre PHT, dont la taille suit une suite géométrique. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. Le prédicteur TAGE a été raffiné avec le temps, sur plusieurs points, que je ne vais pas détailler dans ce qui suit. Précisons cependant que les PHT sont structurées en caches, afin d'éliminer les phénomènes d'''aliasing''. L'ajout d'une unité de prédiction pour les boucles, a donné le '''prédicteur L-TAGE''', qui a lui-même été amélioré par la suite. Les unités de branchement TAGE sont les plus utilisées au moment où j'écris ces lignes (2026), et la majorité des processeurs x86 commerciaux utilisent une variante de ce prédicteur, à quelques exceptions près. ===La méta-prédiction=== La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment. ===La fusion de prédictions=== Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée, car elle demande un nombre d'unités important (au moins 4). L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] ===La priorisation des unités de prédiction=== Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> dax62knhc60o397y2wcu4757vstghnc 773875 773871 2026-10-01T16:57:31Z Mewtow 31375 /* Les unités de prédiction à deux niveaux locales */ 773875 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents. Ce registre d'historique est un registre à décalage mis à jour à chaque fois que le processeur exécute un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction. L'unité de prédiction globale utilise un compteur à saturation pour chaque valeur possible du registre d'historique. En clair, pour un registre de N bits, elle utilise 2^n compteurs à saturation. En clair, il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Avec un registre d'historique, la prédiction tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. L'unité de prédiction précédente est très simple et il existe des unités de prédiction similaires, basés sur des principes quelque peu différentes. Elles sont appelées des '''unités de prédiction à deux niveaux''', ce qui sous-entend que le registre d'historique et la PHT sont deux niveaux différents. Elles peuvent utiliser soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment, les unités de prédiction avec plusieurs PHTs seront vues dans la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] Dans l'unité de prédiction précédente, le choix du compteur à saturation utiliser se fait uniquement grâce au registre d'historique global. les unités que nous allons voir font un choix plus complexe, qui utilisent d'autres informations. Mais pourquoi utiliser d'autres informations ? Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Les unités de prédiction précédentes utilisaient une PHT globale, ce qui entrainait des problèmes d’''aliasing''. Une solution alternative utilise une PHT par branchement, tout en conservant un historique global. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. Chaque PHT associée à un branchement est appelée une '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] Au lieu d'un historique global, pour tous les branchements, il est possible d'utiliser un historique pour chaque branchement. Précisément, pour chaque entrée dans le BTB, le BTB mémorise le résultat des n dernières exécutions du branchement associé. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. Un tel historique est appelé un '''historique local'''. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout en transistor élevé, avec tous les défauts que ca implique en termes de consommation électrique et thermique. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées seules du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ===La prédiction des branchements des boucles=== Les unités de prédiction avec un historique marchent très bien pour prédire les branchements des if...else, mais elles sont inadaptées pour prédire les branchements des boucles. L'usage de la prédiction statique ou de compteurs à saturation permet d'obtenir de meilleures performances pour ce cas de figure. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Dans ces conditions, la prédiction statique marche très bien pour les boucles, notamment les boucles FOR. De telles unités prédisent que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Rappelons qu'une boucle FOR répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ==La prédiction de branchement à deux niveaux== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. ===Le prédicteur L-TAGE=== Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Une méta-unité de branchement prédit alors quelle taille d'historique donne les meilleurs résultats. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. C'est d'ailleurs le principe derrière les '''unités de branchement TAGE (''TAgged GEometric length predictor'')'''. Elles utilisent plusieurs registres d'historiques, chacun avec sa propre PHT, dont la taille suit une suite géométrique. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. Le prédicteur TAGE a été raffiné avec le temps, sur plusieurs points, que je ne vais pas détailler dans ce qui suit. Précisons cependant que les PHT sont structurées en caches, afin d'éliminer les phénomènes d'''aliasing''. L'ajout d'une unité de prédiction pour les boucles, a donné le '''prédicteur L-TAGE''', qui a lui-même été amélioré par la suite. Les unités de branchement TAGE sont les plus utilisées au moment où j'écris ces lignes (2026), et la majorité des processeurs x86 commerciaux utilisent une variante de ce prédicteur, à quelques exceptions près. ===La méta-prédiction=== La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment. ===La fusion de prédictions=== Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée, car elle demande un nombre d'unités important (au moins 4). L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] ===La priorisation des unités de prédiction=== Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> qssz3s6r6vxfyu34h55jqj1gm8dl4tm 773876 773875 2026-10-01T17:04:31Z Mewtow 31375 /* La prédiction des branchements des boucles */ 773876 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents. Ce registre d'historique est un registre à décalage mis à jour à chaque fois que le processeur exécute un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction. L'unité de prédiction globale utilise un compteur à saturation pour chaque valeur possible du registre d'historique. En clair, pour un registre de N bits, elle utilise 2^n compteurs à saturation. En clair, il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Avec un registre d'historique, la prédiction tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. L'unité de prédiction précédente est très simple et il existe des unités de prédiction similaires, basés sur des principes quelque peu différentes. Elles sont appelées des '''unités de prédiction à deux niveaux''', ce qui sous-entend que le registre d'historique et la PHT sont deux niveaux différents. Elles peuvent utiliser soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment, les unités de prédiction avec plusieurs PHTs seront vues dans la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] Dans l'unité de prédiction précédente, le choix du compteur à saturation utiliser se fait uniquement grâce au registre d'historique global. les unités que nous allons voir font un choix plus complexe, qui utilisent d'autres informations. Mais pourquoi utiliser d'autres informations ? Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Les unités de prédiction précédentes utilisaient une PHT globale, ce qui entrainait des problèmes d’''aliasing''. Une solution alternative utilise une PHT par branchement, tout en conservant un historique global. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. Chaque PHT associée à un branchement est appelée une '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] Au lieu d'un historique global, pour tous les branchements, il est possible d'utiliser un historique pour chaque branchement. Précisément, pour chaque entrée dans le BTB, le BTB mémorise le résultat des n dernières exécutions du branchement associé. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. Un tel historique est appelé un '''historique local'''. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout en transistor élevé, avec tous les défauts que ca implique en termes de consommation électrique et thermique. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées seules du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ===La prédiction des branchements des boucles=== Les boucles sont à part, pour ce qui est de la prédiction de branchement. Rappelons qu'une boucle FOR/WHILE répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. La conséquence est que les unités de prédiction usuelles sont assez mauvaises pour prédire les branchements des boucles. Par contre, la prédiction statique marche particulièrement bien comparé au reste. La prédiction statique prédit que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Reste à gérer les branchements des boucles au mieux, en sachant cela. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ==La prédiction de branchement à deux niveaux== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. ===Le prédicteur L-TAGE=== Il est par exemple possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Une méta-unité de branchement prédit alors quelle taille d'historique donne les meilleurs résultats. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. C'est d'ailleurs le principe derrière les '''unités de branchement TAGE (''TAgged GEometric length predictor'')'''. Elles utilisent plusieurs registres d'historiques, chacun avec sa propre PHT, dont la taille suit une suite géométrique. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. Le prédicteur TAGE a été raffiné avec le temps, sur plusieurs points, que je ne vais pas détailler dans ce qui suit. Précisons cependant que les PHT sont structurées en caches, afin d'éliminer les phénomènes d'''aliasing''. L'ajout d'une unité de prédiction pour les boucles, a donné le '''prédicteur L-TAGE''', qui a lui-même été amélioré par la suite. Les unités de branchement TAGE sont les plus utilisées au moment où j'écris ces lignes (2026), et la majorité des processeurs x86 commerciaux utilisent une variante de ce prédicteur, à quelques exceptions près. ===La méta-prédiction=== La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment. ===La fusion de prédictions=== Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée, car elle demande un nombre d'unités important (au moins 4). L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] ===La priorisation des unités de prédiction=== Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> 2bdql7qz9lkzsp8vwg3wsak4k77whyd 773877 773876 2026-10-01T17:04:55Z Mewtow 31375 /* Le prédicteur L-TAGE */ 773877 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents. Ce registre d'historique est un registre à décalage mis à jour à chaque fois que le processeur exécute un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction. L'unité de prédiction globale utilise un compteur à saturation pour chaque valeur possible du registre d'historique. En clair, pour un registre de N bits, elle utilise 2^n compteurs à saturation. En clair, il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Avec un registre d'historique, la prédiction tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. L'unité de prédiction précédente est très simple et il existe des unités de prédiction similaires, basés sur des principes quelque peu différentes. Elles sont appelées des '''unités de prédiction à deux niveaux''', ce qui sous-entend que le registre d'historique et la PHT sont deux niveaux différents. Elles peuvent utiliser soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment, les unités de prédiction avec plusieurs PHTs seront vues dans la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] Dans l'unité de prédiction précédente, le choix du compteur à saturation utiliser se fait uniquement grâce au registre d'historique global. les unités que nous allons voir font un choix plus complexe, qui utilisent d'autres informations. Mais pourquoi utiliser d'autres informations ? Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Les unités de prédiction précédentes utilisaient une PHT globale, ce qui entrainait des problèmes d’''aliasing''. Une solution alternative utilise une PHT par branchement, tout en conservant un historique global. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. Chaque PHT associée à un branchement est appelée une '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] Au lieu d'un historique global, pour tous les branchements, il est possible d'utiliser un historique pour chaque branchement. Précisément, pour chaque entrée dans le BTB, le BTB mémorise le résultat des n dernières exécutions du branchement associé. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. Un tel historique est appelé un '''historique local'''. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout en transistor élevé, avec tous les défauts que ca implique en termes de consommation électrique et thermique. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées seules du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ===La prédiction des branchements des boucles=== Les boucles sont à part, pour ce qui est de la prédiction de branchement. Rappelons qu'une boucle FOR/WHILE répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. La conséquence est que les unités de prédiction usuelles sont assez mauvaises pour prédire les branchements des boucles. Par contre, la prédiction statique marche particulièrement bien comparé au reste. La prédiction statique prédit que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Reste à gérer les branchements des boucles au mieux, en sachant cela. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ==La prédiction de branchement à deux niveaux== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. ===Le prédicteur L-TAGE=== Il est possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Une méta-unité de branchement prédit alors quelle taille d'historique donne les meilleurs résultats. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. C'est d'ailleurs le principe derrière les '''unités de branchement TAGE (''TAgged GEometric length predictor'')'''. Elles utilisent plusieurs registres d'historiques, chacun avec sa propre PHT, dont la taille suit une suite géométrique. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. Le prédicteur TAGE a été raffiné avec le temps, sur plusieurs points, que je ne vais pas détailler dans ce qui suit. Précisons cependant que les PHT sont structurées en caches, afin d'éliminer les phénomènes d'''aliasing''. L'ajout d'une unité de prédiction pour les boucles, a donné le '''prédicteur L-TAGE''', qui a lui-même été amélioré par la suite. Les unités de branchement TAGE sont les plus utilisées au moment où j'écris ces lignes (2026), et la majorité des processeurs x86 commerciaux utilisent une variante de ce prédicteur, à quelques exceptions près. ===La méta-prédiction=== La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment. ===La fusion de prédictions=== Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée, car elle demande un nombre d'unités important (au moins 4). L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] ===La priorisation des unités de prédiction=== Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> 58gq52hs5y298n0wnd28r6xqzlyuirf 773878 773877 2026-10-01T17:05:18Z Mewtow 31375 /* La prédiction des branchements des boucles */ 773878 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents. Ce registre d'historique est un registre à décalage mis à jour à chaque fois que le processeur exécute un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction. L'unité de prédiction globale utilise un compteur à saturation pour chaque valeur possible du registre d'historique. En clair, pour un registre de N bits, elle utilise 2^n compteurs à saturation. En clair, il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Avec un registre d'historique, la prédiction tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. L'unité de prédiction précédente est très simple et il existe des unités de prédiction similaires, basés sur des principes quelque peu différentes. Elles sont appelées des '''unités de prédiction à deux niveaux''', ce qui sous-entend que le registre d'historique et la PHT sont deux niveaux différents. Elles peuvent utiliser soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment, les unités de prédiction avec plusieurs PHTs seront vues dans la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] Dans l'unité de prédiction précédente, le choix du compteur à saturation utiliser se fait uniquement grâce au registre d'historique global. les unités que nous allons voir font un choix plus complexe, qui utilisent d'autres informations. Mais pourquoi utiliser d'autres informations ? Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Les unités de prédiction précédentes utilisaient une PHT globale, ce qui entrainait des problèmes d’''aliasing''. Une solution alternative utilise une PHT par branchement, tout en conservant un historique global. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. Chaque PHT associée à un branchement est appelée une '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] Au lieu d'un historique global, pour tous les branchements, il est possible d'utiliser un historique pour chaque branchement. Précisément, pour chaque entrée dans le BTB, le BTB mémorise le résultat des n dernières exécutions du branchement associé. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. Un tel historique est appelé un '''historique local'''. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout en transistor élevé, avec tous les défauts que ca implique en termes de consommation électrique et thermique. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées seules du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ==La prédiction de branchement à deux niveaux== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. ===Le prédicteur L-TAGE=== Il est possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Une méta-unité de branchement prédit alors quelle taille d'historique donne les meilleurs résultats. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. C'est d'ailleurs le principe derrière les '''unités de branchement TAGE (''TAgged GEometric length predictor'')'''. Elles utilisent plusieurs registres d'historiques, chacun avec sa propre PHT, dont la taille suit une suite géométrique. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. Le prédicteur TAGE a été raffiné avec le temps, sur plusieurs points, que je ne vais pas détailler dans ce qui suit. Précisons cependant que les PHT sont structurées en caches, afin d'éliminer les phénomènes d'''aliasing''. L'ajout d'une unité de prédiction pour les boucles, a donné le '''prédicteur L-TAGE''', qui a lui-même été amélioré par la suite. Les unités de branchement TAGE sont les plus utilisées au moment où j'écris ces lignes (2026), et la majorité des processeurs x86 commerciaux utilisent une variante de ce prédicteur, à quelques exceptions près. ===La méta-prédiction=== La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment. ===La fusion de prédictions=== Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée, car elle demande un nombre d'unités important (au moins 4). L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] ===La priorisation des unités de prédiction=== Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> d4cinwrqu8iutfyfw6aijqreaeinmfm 773879 773878 2026-10-01T17:05:26Z Mewtow 31375 /* Le prédicteur L-TAGE */ 773879 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents. Ce registre d'historique est un registre à décalage mis à jour à chaque fois que le processeur exécute un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction. L'unité de prédiction globale utilise un compteur à saturation pour chaque valeur possible du registre d'historique. En clair, pour un registre de N bits, elle utilise 2^n compteurs à saturation. En clair, il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Avec un registre d'historique, la prédiction tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. L'unité de prédiction précédente est très simple et il existe des unités de prédiction similaires, basés sur des principes quelque peu différentes. Elles sont appelées des '''unités de prédiction à deux niveaux''', ce qui sous-entend que le registre d'historique et la PHT sont deux niveaux différents. Elles peuvent utiliser soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment, les unités de prédiction avec plusieurs PHTs seront vues dans la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] Dans l'unité de prédiction précédente, le choix du compteur à saturation utiliser se fait uniquement grâce au registre d'historique global. les unités que nous allons voir font un choix plus complexe, qui utilisent d'autres informations. Mais pourquoi utiliser d'autres informations ? Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Les unités de prédiction précédentes utilisaient une PHT globale, ce qui entrainait des problèmes d’''aliasing''. Une solution alternative utilise une PHT par branchement, tout en conservant un historique global. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. Chaque PHT associée à un branchement est appelée une '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] Au lieu d'un historique global, pour tous les branchements, il est possible d'utiliser un historique pour chaque branchement. Précisément, pour chaque entrée dans le BTB, le BTB mémorise le résultat des n dernières exécutions du branchement associé. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. Un tel historique est appelé un '''historique local'''. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout en transistor élevé, avec tous les défauts que ca implique en termes de consommation électrique et thermique. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées seules du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ==La prédiction de branchement à deux niveaux== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. ===La prédiction des branchements des boucles=== Les boucles sont à part, pour ce qui est de la prédiction de branchement. Rappelons qu'une boucle FOR/WHILE répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. La conséquence est que les unités de prédiction usuelles sont assez mauvaises pour prédire les branchements des boucles. Par contre, la prédiction statique marche particulièrement bien comparé au reste. La prédiction statique prédit que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Reste à gérer les branchements des boucles au mieux, en sachant cela. L'idée est d'utiliser deux unités de prédiction séparées : une unité avec un historique, et une autre spécialisée dans les boucles. Concrètement, les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ===Le prédicteur L-TAGE=== Il est possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Une méta-unité de branchement prédit alors quelle taille d'historique donne les meilleurs résultats. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. C'est d'ailleurs le principe derrière les '''unités de branchement TAGE (''TAgged GEometric length predictor'')'''. Elles utilisent plusieurs registres d'historiques, chacun avec sa propre PHT, dont la taille suit une suite géométrique. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. Le prédicteur TAGE a été raffiné avec le temps, sur plusieurs points, que je ne vais pas détailler dans ce qui suit. Précisons cependant que les PHT sont structurées en caches, afin d'éliminer les phénomènes d'''aliasing''. L'ajout d'une unité de prédiction pour les boucles, a donné le '''prédicteur L-TAGE''', qui a lui-même été amélioré par la suite. Les unités de branchement TAGE sont les plus utilisées au moment où j'écris ces lignes (2026), et la majorité des processeurs x86 commerciaux utilisent une variante de ce prédicteur, à quelques exceptions près. ===La méta-prédiction=== La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Le circuit méta-prédicteur est conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. On peut aussi utiliser toute autre technique de prédiction vue plus haut, mais cela demande alors plus de circuits. C'est cette technique qui est utilisée dans l'unité de prédiction bimodale vue précédemment. ===La fusion de prédictions=== Une autre solution est de combiner le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas. Il y a un risque que la moitié des unités de prédiction donne un résultat différent de l'autre moitié. Et faire un choix dans ce cas précis est rarement facile. Une solution serait de prioriser certaines unités, qui auraient un poids plus important dans le calcul de la majorité, de la moyenne, mais elle est rarement appliquée, car elle demande un nombre d'unités important (au moins 4). L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] ===La priorisation des unités de prédiction=== Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> 93hhop1aoflllwzmo8hoi5a9xcrp7l4 773880 773879 2026-10-01T17:08:37Z Mewtow 31375 /* La prédiction avec plusieurs unités de branchement distinctes */ 773880 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents. Ce registre d'historique est un registre à décalage mis à jour à chaque fois que le processeur exécute un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction. L'unité de prédiction globale utilise un compteur à saturation pour chaque valeur possible du registre d'historique. En clair, pour un registre de N bits, elle utilise 2^n compteurs à saturation. En clair, il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Avec un registre d'historique, la prédiction tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. L'unité de prédiction précédente est très simple et il existe des unités de prédiction similaires, basés sur des principes quelque peu différentes. Elles sont appelées des '''unités de prédiction à deux niveaux''', ce qui sous-entend que le registre d'historique et la PHT sont deux niveaux différents. Elles peuvent utiliser soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment, les unités de prédiction avec plusieurs PHTs seront vues dans la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] Dans l'unité de prédiction précédente, le choix du compteur à saturation utiliser se fait uniquement grâce au registre d'historique global. les unités que nous allons voir font un choix plus complexe, qui utilisent d'autres informations. Mais pourquoi utiliser d'autres informations ? Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Les unités de prédiction précédentes utilisaient une PHT globale, ce qui entrainait des problèmes d’''aliasing''. Une solution alternative utilise une PHT par branchement, tout en conservant un historique global. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. Chaque PHT associée à un branchement est appelée une '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] Au lieu d'un historique global, pour tous les branchements, il est possible d'utiliser un historique pour chaque branchement. Précisément, pour chaque entrée dans le BTB, le BTB mémorise le résultat des n dernières exécutions du branchement associé. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. Un tel historique est appelé un '''historique local'''. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout en transistor élevé, avec tous les défauts que ca implique en termes de consommation électrique et thermique. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées seules du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ==La prédiction de branchement à deux niveaux== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Il arrive que le circuit méta-prédicteur soit conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. Mais c'est loin d'être systématique. Une autre solution combine le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas, il y a un risque d'égalité. L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] ===La prédiction des branchements des boucles=== Les boucles sont à part, pour ce qui est de la prédiction de branchement. Rappelons qu'une boucle FOR/WHILE répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. La conséquence est que les unités de prédiction usuelles sont assez mauvaises pour prédire les branchements des boucles. Par contre, la prédiction statique marche particulièrement bien comparé au reste. La prédiction statique prédit que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Reste à gérer les branchements des boucles au mieux, en sachant cela. L'idée est d'utiliser plusieurs unités de prédiction séparées : une spécialisée dans les boucles et d'autres unités de prédiction. Uen unité de méta-prédiction se charge d'activer l'unité de prédiction des boucles dès qu'une boucle est détéctée. Les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ===Le prédicteur L-TAGE=== Il est possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Une méta-unité de branchement prédit alors quelle taille d'historique donne les meilleurs résultats. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. C'est d'ailleurs le principe derrière les '''unités de branchement TAGE (''TAgged GEometric length predictor'')'''. Elles utilisent plusieurs registres d'historiques, chacun avec sa propre PHT, dont la taille suit une suite géométrique. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. Le prédicteur TAGE a été raffiné avec le temps, sur plusieurs points, que je ne vais pas détailler dans ce qui suit. Précisons cependant que les PHT sont structurées en caches, afin d'éliminer les phénomènes d'''aliasing''. L'ajout d'une unité de prédiction pour les boucles, a donné le '''prédicteur L-TAGE''', qui a lui-même été amélioré par la suite. Les unités de branchement TAGE sont les plus utilisées au moment où j'écris ces lignes (2026), et la majorité des processeurs x86 commerciaux utilisent une variante de ce prédicteur, à quelques exceptions près. ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> gt0f7dk7kjd54awpr26e03jv89y26uj 773881 773880 2026-10-01T17:09:04Z Mewtow 31375 /* La prédiction de branchement à deux niveaux */ 773881 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents. Ce registre d'historique est un registre à décalage mis à jour à chaque fois que le processeur exécute un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction. L'unité de prédiction globale utilise un compteur à saturation pour chaque valeur possible du registre d'historique. En clair, pour un registre de N bits, elle utilise 2^n compteurs à saturation. En clair, il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Avec un registre d'historique, la prédiction tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. L'unité de prédiction précédente est très simple et il existe des unités de prédiction similaires, basés sur des principes quelque peu différentes. Elles sont appelées des '''unités de prédiction à deux niveaux''', ce qui sous-entend que le registre d'historique et la PHT sont deux niveaux différents. Elles peuvent utiliser soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment, les unités de prédiction avec plusieurs PHTs seront vues dans la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] Dans l'unité de prédiction précédente, le choix du compteur à saturation utiliser se fait uniquement grâce au registre d'historique global. les unités que nous allons voir font un choix plus complexe, qui utilisent d'autres informations. Mais pourquoi utiliser d'autres informations ? Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Les unités de prédiction précédentes utilisaient une PHT globale, ce qui entrainait des problèmes d’''aliasing''. Une solution alternative utilise une PHT par branchement, tout en conservant un historique global. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. Chaque PHT associée à un branchement est appelée une '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] Au lieu d'un historique global, pour tous les branchements, il est possible d'utiliser un historique pour chaque branchement. Précisément, pour chaque entrée dans le BTB, le BTB mémorise le résultat des n dernières exécutions du branchement associé. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. Un tel historique est appelé un '''historique local'''. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout en transistor élevé, avec tous les défauts que ca implique en termes de consommation électrique et thermique. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées seules du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Il arrive que le circuit méta-prédicteur soit conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. Mais c'est loin d'être systématique. Une autre solution combine le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas, il y a un risque d'égalité. L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] ===La prédiction des branchements des boucles=== Les boucles sont à part, pour ce qui est de la prédiction de branchement. Rappelons qu'une boucle FOR/WHILE répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. La conséquence est que les unités de prédiction usuelles sont assez mauvaises pour prédire les branchements des boucles. Par contre, la prédiction statique marche particulièrement bien comparé au reste. La prédiction statique prédit que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Reste à gérer les branchements des boucles au mieux, en sachant cela. L'idée est d'utiliser plusieurs unités de prédiction séparées : une spécialisée dans les boucles et d'autres unités de prédiction. Uen unité de méta-prédiction se charge d'activer l'unité de prédiction des boucles dès qu'une boucle est détéctée. Les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ===Le prédicteur L-TAGE=== Il est possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Une méta-unité de branchement prédit alors quelle taille d'historique donne les meilleurs résultats. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. C'est d'ailleurs le principe derrière les '''unités de branchement TAGE (''TAgged GEometric length predictor'')'''. Elles utilisent plusieurs registres d'historiques, chacun avec sa propre PHT, dont la taille suit une suite géométrique. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. Le prédicteur TAGE a été raffiné avec le temps, sur plusieurs points, que je ne vais pas détailler dans ce qui suit. Précisons cependant que les PHT sont structurées en caches, afin d'éliminer les phénomènes d'''aliasing''. L'ajout d'une unité de prédiction pour les boucles, a donné le '''prédicteur L-TAGE''', qui a lui-même été amélioré par la suite. Les unités de branchement TAGE sont les plus utilisées au moment où j'écris ces lignes (2026), et la majorité des processeurs x86 commerciaux utilisent une variante de ce prédicteur, à quelques exceptions près. ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> t5dt7cyxaq1a5r1vyqj56y9asj6hhq9 773882 773881 2026-10-01T17:09:18Z Mewtow 31375 /* Le prédicteur L-TAGE */ 773882 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents. Ce registre d'historique est un registre à décalage mis à jour à chaque fois que le processeur exécute un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction. L'unité de prédiction globale utilise un compteur à saturation pour chaque valeur possible du registre d'historique. En clair, pour un registre de N bits, elle utilise 2^n compteurs à saturation. En clair, il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Avec un registre d'historique, la prédiction tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. L'unité de prédiction précédente est très simple et il existe des unités de prédiction similaires, basés sur des principes quelque peu différentes. Elles sont appelées des '''unités de prédiction à deux niveaux''', ce qui sous-entend que le registre d'historique et la PHT sont deux niveaux différents. Elles peuvent utiliser soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment, les unités de prédiction avec plusieurs PHTs seront vues dans la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] Dans l'unité de prédiction précédente, le choix du compteur à saturation utiliser se fait uniquement grâce au registre d'historique global. les unités que nous allons voir font un choix plus complexe, qui utilisent d'autres informations. Mais pourquoi utiliser d'autres informations ? Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Les unités de prédiction précédentes utilisaient une PHT globale, ce qui entrainait des problèmes d’''aliasing''. Une solution alternative utilise une PHT par branchement, tout en conservant un historique global. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. Chaque PHT associée à un branchement est appelée une '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] Au lieu d'un historique global, pour tous les branchements, il est possible d'utiliser un historique pour chaque branchement. Précisément, pour chaque entrée dans le BTB, le BTB mémorise le résultat des n dernières exécutions du branchement associé. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. Un tel historique est appelé un '''historique local'''. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout en transistor élevé, avec tous les défauts que ca implique en termes de consommation électrique et thermique. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées seules du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Il arrive que le circuit méta-prédicteur soit conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. Mais c'est loin d'être systématique. Une autre solution combine le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas, il y a un risque d'égalité. L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] ===La prédiction des branchements des boucles=== Les boucles sont à part, pour ce qui est de la prédiction de branchement. Rappelons qu'une boucle FOR/WHILE répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. La conséquence est que les unités de prédiction usuelles sont assez mauvaises pour prédire les branchements des boucles. Par contre, la prédiction statique marche particulièrement bien comparé au reste. La prédiction statique prédit que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Reste à gérer les branchements des boucles au mieux, en sachant cela. L'idée est d'utiliser plusieurs unités de prédiction séparées : une spécialisée dans les boucles et d'autres unités de prédiction. Uen unité de méta-prédiction se charge d'activer l'unité de prédiction des boucles dès qu'une boucle est détéctée. Les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ===Le prédicteur L-TAGE=== Il est possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Une méta-unité de branchement prédit alors quelle taille d'historique donne les meilleurs résultats. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. C'est d'ailleurs le principe derrière les '''unités de branchement TAGE (''TAgged GEometric length predictor'')'''. Elles utilisent plusieurs registres d'historiques, chacun avec sa propre PHT, dont la taille suit une suite géométrique. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. Le prédicteur TAGE a été raffiné avec le temps, sur plusieurs points, que je ne vais pas détailler dans ce qui suit. Précisons cependant que les PHT sont structurées en caches, afin d'éliminer les phénomènes d'''aliasing''. L'ajout d'une unité de prédiction pour les boucles, a donné le '''prédicteur L-TAGE''', qui a lui-même été amélioré par la suite. Les unités de branchement TAGE sont les plus utilisées au moment où j'écris ces lignes (2026), et la majorité des processeurs x86 commerciaux utilisent une variante de ce prédicteur, à quelques exceptions près. ===La prédiction de branchement à deux niveaux=== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> eg6ce5pr8i91t22k0m5tecwzh851iz9 773883 773882 2026-10-01T17:10:08Z Mewtow 31375 /* La prédiction avec plusieurs unités de branchement distinctes */ 773883 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents. Ce registre d'historique est un registre à décalage mis à jour à chaque fois que le processeur exécute un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction. L'unité de prédiction globale utilise un compteur à saturation pour chaque valeur possible du registre d'historique. En clair, pour un registre de N bits, elle utilise 2^n compteurs à saturation. En clair, il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Avec un registre d'historique, la prédiction tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. L'unité de prédiction précédente est très simple et il existe des unités de prédiction similaires, basés sur des principes quelque peu différentes. Elles sont appelées des '''unités de prédiction à deux niveaux''', ce qui sous-entend que le registre d'historique et la PHT sont deux niveaux différents. Elles peuvent utiliser soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment, les unités de prédiction avec plusieurs PHTs seront vues dans la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] Dans l'unité de prédiction précédente, le choix du compteur à saturation utiliser se fait uniquement grâce au registre d'historique global. les unités que nous allons voir font un choix plus complexe, qui utilisent d'autres informations. Mais pourquoi utiliser d'autres informations ? Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Les unités de prédiction précédentes utilisaient une PHT globale, ce qui entrainait des problèmes d’''aliasing''. Une solution alternative utilise une PHT par branchement, tout en conservant un historique global. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. Chaque PHT associée à un branchement est appelée une '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] Au lieu d'un historique global, pour tous les branchements, il est possible d'utiliser un historique pour chaque branchement. Précisément, pour chaque entrée dans le BTB, le BTB mémorise le résultat des n dernières exécutions du branchement associé. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. Un tel historique est appelé un '''historique local'''. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout en transistor élevé, avec tous les défauts que ca implique en termes de consommation électrique et thermique. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées seules du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Il arrive que le circuit méta-prédicteur soit conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. Mais c'est loin d'être systématique. [[File:Partial tag matching.png|centre|vignette|upright=2|Partial tag matching.]] Une autre solution combine le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas, il y a un risque d'égalité. L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. ===La prédiction des branchements des boucles=== Les boucles sont à part, pour ce qui est de la prédiction de branchement. Rappelons qu'une boucle FOR/WHILE répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. La conséquence est que les unités de prédiction usuelles sont assez mauvaises pour prédire les branchements des boucles. Par contre, la prédiction statique marche particulièrement bien comparé au reste. La prédiction statique prédit que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Reste à gérer les branchements des boucles au mieux, en sachant cela. L'idée est d'utiliser plusieurs unités de prédiction séparées : une spécialisée dans les boucles et d'autres unités de prédiction. Uen unité de méta-prédiction se charge d'activer l'unité de prédiction des boucles dès qu'une boucle est détéctée. Les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ===Le prédicteur L-TAGE=== Il est possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Une méta-unité de branchement prédit alors quelle taille d'historique donne les meilleurs résultats. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. C'est d'ailleurs le principe derrière les '''unités de branchement TAGE (''TAgged GEometric length predictor'')'''. Elles utilisent plusieurs registres d'historiques, chacun avec sa propre PHT, dont la taille suit une suite géométrique. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. Le prédicteur TAGE a été raffiné avec le temps, sur plusieurs points, que je ne vais pas détailler dans ce qui suit. Précisons cependant que les PHT sont structurées en caches, afin d'éliminer les phénomènes d'''aliasing''. L'ajout d'une unité de prédiction pour les boucles, a donné le '''prédicteur L-TAGE''', qui a lui-même été amélioré par la suite. Les unités de branchement TAGE sont les plus utilisées au moment où j'écris ces lignes (2026), et la majorité des processeurs x86 commerciaux utilisent une variante de ce prédicteur, à quelques exceptions près. ===La prédiction de branchement à deux niveaux=== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> ev5hze8bs2tb021uve8m4m2z5vz3bcp 773884 773883 2026-10-01T17:10:46Z Mewtow 31375 /* La prédiction avec plusieurs unités de branchement distinctes */ 773884 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents. Ce registre d'historique est un registre à décalage mis à jour à chaque fois que le processeur exécute un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction. L'unité de prédiction globale utilise un compteur à saturation pour chaque valeur possible du registre d'historique. En clair, pour un registre de N bits, elle utilise 2^n compteurs à saturation. En clair, il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Avec un registre d'historique, la prédiction tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. L'unité de prédiction précédente est très simple et il existe des unités de prédiction similaires, basés sur des principes quelque peu différentes. Elles sont appelées des '''unités de prédiction à deux niveaux''', ce qui sous-entend que le registre d'historique et la PHT sont deux niveaux différents. Elles peuvent utiliser soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment, les unités de prédiction avec plusieurs PHTs seront vues dans la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] Dans l'unité de prédiction précédente, le choix du compteur à saturation utiliser se fait uniquement grâce au registre d'historique global. les unités que nous allons voir font un choix plus complexe, qui utilisent d'autres informations. Mais pourquoi utiliser d'autres informations ? Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Les unités de prédiction précédentes utilisaient une PHT globale, ce qui entrainait des problèmes d’''aliasing''. Une solution alternative utilise une PHT par branchement, tout en conservant un historique global. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. Chaque PHT associée à un branchement est appelée une '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] Au lieu d'un historique global, pour tous les branchements, il est possible d'utiliser un historique pour chaque branchement. Précisément, pour chaque entrée dans le BTB, le BTB mémorise le résultat des n dernières exécutions du branchement associé. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. Un tel historique est appelé un '''historique local'''. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout en transistor élevé, avec tous les défauts que ca implique en termes de consommation électrique et thermique. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées seules du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Il arrive que le circuit méta-prédicteur soit conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. Mais c'est loin d'être systématique. Une autre solution combine le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas, il y a un risque d'égalité. L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] Une autre technique consiste à prioriser les unités de prédiction de branchement. Une unité de prédiction complexe est utilisé en premier, mais une seconde unité plus simple (généralement des compteurs à saturation) prend le relai en cas de non-prédiction. ===La prédiction des branchements des boucles=== Les boucles sont à part, pour ce qui est de la prédiction de branchement. Rappelons qu'une boucle FOR/WHILE répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. La conséquence est que les unités de prédiction usuelles sont assez mauvaises pour prédire les branchements des boucles. Par contre, la prédiction statique marche particulièrement bien comparé au reste. La prédiction statique prédit que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Reste à gérer les branchements des boucles au mieux, en sachant cela. L'idée est d'utiliser plusieurs unités de prédiction séparées : une spécialisée dans les boucles et d'autres unités de prédiction. Uen unité de méta-prédiction se charge d'activer l'unité de prédiction des boucles dès qu'une boucle est détéctée. Les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ===Le prédicteur L-TAGE=== Il est possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Une méta-unité de branchement prédit alors quelle taille d'historique donne les meilleurs résultats. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. C'est d'ailleurs le principe derrière les '''unités de branchement TAGE (''TAgged GEometric length predictor'')'''. Elles utilisent plusieurs registres d'historiques, chacun avec sa propre PHT, dont la taille suit une suite géométrique. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. Le prédicteur TAGE a été raffiné avec le temps, sur plusieurs points, que je ne vais pas détailler dans ce qui suit. Précisons cependant que les PHT sont structurées en caches, afin d'éliminer les phénomènes d'''aliasing''. L'ajout d'une unité de prédiction pour les boucles, a donné le '''prédicteur L-TAGE''', qui a lui-même été amélioré par la suite. Les unités de branchement TAGE sont les plus utilisées au moment où j'écris ces lignes (2026), et la majorité des processeurs x86 commerciaux utilisent une variante de ce prédicteur, à quelques exceptions près. ===La prédiction de branchement à deux niveaux=== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> a90373cdx3rl2o9c3y4qjtaosxmklsc 773885 773884 2026-10-01T17:11:00Z Mewtow 31375 /* La prédiction avec plusieurs unités de branchement distinctes */ 773885 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents. Ce registre d'historique est un registre à décalage mis à jour à chaque fois que le processeur exécute un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction. L'unité de prédiction globale utilise un compteur à saturation pour chaque valeur possible du registre d'historique. En clair, pour un registre de N bits, elle utilise 2^n compteurs à saturation. En clair, il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Avec un registre d'historique, la prédiction tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. L'unité de prédiction précédente est très simple et il existe des unités de prédiction similaires, basés sur des principes quelque peu différentes. Elles sont appelées des '''unités de prédiction à deux niveaux''', ce qui sous-entend que le registre d'historique et la PHT sont deux niveaux différents. Elles peuvent utiliser soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment, les unités de prédiction avec plusieurs PHTs seront vues dans la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] Dans l'unité de prédiction précédente, le choix du compteur à saturation utiliser se fait uniquement grâce au registre d'historique global. les unités que nous allons voir font un choix plus complexe, qui utilisent d'autres informations. Mais pourquoi utiliser d'autres informations ? Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Les unités de prédiction précédentes utilisaient une PHT globale, ce qui entrainait des problèmes d’''aliasing''. Une solution alternative utilise une PHT par branchement, tout en conservant un historique global. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. Chaque PHT associée à un branchement est appelée une '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] Au lieu d'un historique global, pour tous les branchements, il est possible d'utiliser un historique pour chaque branchement. Précisément, pour chaque entrée dans le BTB, le BTB mémorise le résultat des n dernières exécutions du branchement associé. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. Un tel historique est appelé un '''historique local'''. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout en transistor élevé, avec tous les défauts que ca implique en termes de consommation électrique et thermique. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées seules du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Il arrive que le circuit méta-prédicteur soit conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. Mais c'est loin d'être systématique. Une autre solution combine le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas, il y a un risque d'égalité. L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] ===La prédiction des branchements des boucles=== Les boucles sont à part, pour ce qui est de la prédiction de branchement. Rappelons qu'une boucle FOR/WHILE répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. La conséquence est que les unités de prédiction usuelles sont assez mauvaises pour prédire les branchements des boucles. Par contre, la prédiction statique marche particulièrement bien comparé au reste. La prédiction statique prédit que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Reste à gérer les branchements des boucles au mieux, en sachant cela. L'idée est d'utiliser plusieurs unités de prédiction séparées : une spécialisée dans les boucles et d'autres unités de prédiction. Uen unité de méta-prédiction se charge d'activer l'unité de prédiction des boucles dès qu'une boucle est détéctée. Les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ===Le prédicteur L-TAGE=== Il est possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Une méta-unité de branchement prédit alors quelle taille d'historique donne les meilleurs résultats. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. C'est d'ailleurs le principe derrière les '''unités de branchement TAGE (''TAgged GEometric length predictor'')'''. Elles utilisent plusieurs registres d'historiques, chacun avec sa propre PHT, dont la taille suit une suite géométrique. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. Le prédicteur TAGE a été raffiné avec le temps, sur plusieurs points, que je ne vais pas détailler dans ce qui suit. Précisons cependant que les PHT sont structurées en caches, afin d'éliminer les phénomènes d'''aliasing''. L'ajout d'une unité de prédiction pour les boucles, a donné le '''prédicteur L-TAGE''', qui a lui-même été amélioré par la suite. Les unités de branchement TAGE sont les plus utilisées au moment où j'écris ces lignes (2026), et la majorité des processeurs x86 commerciaux utilisent une variante de ce prédicteur, à quelques exceptions près. ===La prédiction de branchement à deux niveaux=== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> egdlxgfc1i1a9e5vyfn4lw9r1g1lmfk 773886 773885 2026-10-01T17:13:02Z Mewtow 31375 /* La prédiction avec plusieurs unités de branchement distinctes */ 773886 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. ===Les contraintes d’implémentation de la prédiction de branchement=== La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents. Ce registre d'historique est un registre à décalage mis à jour à chaque fois que le processeur exécute un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction. L'unité de prédiction globale utilise un compteur à saturation pour chaque valeur possible du registre d'historique. En clair, pour un registre de N bits, elle utilise 2^n compteurs à saturation. En clair, il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Avec un registre d'historique, la prédiction tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. L'unité de prédiction précédente est très simple et il existe des unités de prédiction similaires, basés sur des principes quelque peu différentes. Elles sont appelées des '''unités de prédiction à deux niveaux''', ce qui sous-entend que le registre d'historique et la PHT sont deux niveaux différents. Elles peuvent utiliser soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment, les unités de prédiction avec plusieurs PHTs seront vues dans la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] Dans l'unité de prédiction précédente, le choix du compteur à saturation utiliser se fait uniquement grâce au registre d'historique global. les unités que nous allons voir font un choix plus complexe, qui utilisent d'autres informations. Mais pourquoi utiliser d'autres informations ? Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Les unités de prédiction précédentes utilisaient une PHT globale, ce qui entrainait des problèmes d’''aliasing''. Une solution alternative utilise une PHT par branchement, tout en conservant un historique global. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. Chaque PHT associée à un branchement est appelée une '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] Au lieu d'un historique global, pour tous les branchements, il est possible d'utiliser un historique pour chaque branchement. Précisément, pour chaque entrée dans le BTB, le BTB mémorise le résultat des n dernières exécutions du branchement associé. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. Un tel historique est appelé un '''historique local'''. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout en transistor élevé, avec tous les défauts que ca implique en termes de consommation électrique et thermique. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées seules du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Il arrive que le circuit méta-prédicteur soit conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. Mais c'est loin d'être systématique. Une autre solution combine le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas, il y a un risque d'égalité. L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] Enfin, une dernière solution est que certaines unités de prédiction ont la priorité sur toutes les autres. Par exemple, prenons un processeur avec un ''return stack predictor'', une unité qui détecte les boucles et une unité de prédiction avec un historique. Si le ''return stack predictor'' détecte une instruction de retour de fonction, sa prédiction passera avant toutes les autres unités de prédiction. Et dans ce qui va suivre, c'est cette solution qui sera utilisé pour les unités de prédiction qu'on va étudier. ===La prédiction des branchements des boucles=== Les boucles sont à part, pour ce qui est de la prédiction de branchement. Rappelons qu'une boucle FOR/WHILE répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. La conséquence est que les unités de prédiction usuelles sont assez mauvaises pour prédire les branchements des boucles. Par contre, la prédiction statique marche particulièrement bien comparé au reste. La prédiction statique prédit que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Reste à gérer les branchements des boucles au mieux, en sachant cela. L'idée est d'utiliser plusieurs unités de prédiction séparées : une spécialisée dans les boucles et d'autres unités de prédiction. Uen unité de méta-prédiction se charge d'activer l'unité de prédiction des boucles dès qu'une boucle est détéctée. Les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ===Le prédicteur L-TAGE=== Il est possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Une méta-unité de branchement prédit alors quelle taille d'historique donne les meilleurs résultats. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. C'est d'ailleurs le principe derrière les '''unités de branchement TAGE (''TAgged GEometric length predictor'')'''. Elles utilisent plusieurs registres d'historiques, chacun avec sa propre PHT, dont la taille suit une suite géométrique. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. Le prédicteur TAGE a été raffiné avec le temps, sur plusieurs points, que je ne vais pas détailler dans ce qui suit. Précisons cependant que les PHT sont structurées en caches, afin d'éliminer les phénomènes d'''aliasing''. L'ajout d'une unité de prédiction pour les boucles, a donné le '''prédicteur L-TAGE''', qui a lui-même été amélioré par la suite. Les unités de branchement TAGE sont les plus utilisées au moment où j'écris ces lignes (2026), et la majorité des processeurs x86 commerciaux utilisent une variante de ce prédicteur, à quelques exceptions près. ===La prédiction de branchement à deux niveaux=== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> a3izp5pcrung3x502pv15d8z98divsh 773887 773886 2026-10-01T17:13:27Z Mewtow 31375 /* Les contraintes d’implémentation de la prédiction de branchement */ 773887 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris et décide d'agir en fonction. Si on prédit qu'un branchement est non pris, on continue l’exécution à partir de l'instruction qui suit le branchement. À l'inverse si le branchement est prédit comme étant pris, le processeur devra recourir à l'unité de prédiction de direction de branchement. Maintenant, voyons comment le processeur fait pour prédire si un branchement est pris ou non. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations connues au moment de l’exécution d'un branchement. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante : vider le pipeline est une opération d'autant plus coûteuse que le pipeline est long. Et plus cette pénalité est importante, plus le taux de réussite de l'unité de prédiction doit être important. En conséquence, une simple prédiction fiable à 50% ne tiendra pas la route et il est généralement admis qu'il faut au minimum un taux de réussite proche de 90% de bonnes prédictions, voire plus sur les processeurs modernes. Plus la pénalité en cas de mauvaise prédiction est importante, plus le taux de bonnes prédiction doit être élevé. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. En conséquence, elles doivent être très simples, utiliser des calculs rudimentaires et demander peu de circuits. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée, chose qui est la norme sur les processeurs à haute performance. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents. Ce registre d'historique est un registre à décalage mis à jour à chaque fois que le processeur exécute un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction. L'unité de prédiction globale utilise un compteur à saturation pour chaque valeur possible du registre d'historique. En clair, pour un registre de N bits, elle utilise 2^n compteurs à saturation. En clair, il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Avec un registre d'historique, la prédiction tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. L'unité de prédiction précédente est très simple et il existe des unités de prédiction similaires, basés sur des principes quelque peu différentes. Elles sont appelées des '''unités de prédiction à deux niveaux''', ce qui sous-entend que le registre d'historique et la PHT sont deux niveaux différents. Elles peuvent utiliser soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment, les unités de prédiction avec plusieurs PHTs seront vues dans la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] Dans l'unité de prédiction précédente, le choix du compteur à saturation utiliser se fait uniquement grâce au registre d'historique global. les unités que nous allons voir font un choix plus complexe, qui utilisent d'autres informations. Mais pourquoi utiliser d'autres informations ? Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Les unités de prédiction précédentes utilisaient une PHT globale, ce qui entrainait des problèmes d’''aliasing''. Une solution alternative utilise une PHT par branchement, tout en conservant un historique global. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. Chaque PHT associée à un branchement est appelée une '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] Au lieu d'un historique global, pour tous les branchements, il est possible d'utiliser un historique pour chaque branchement. Précisément, pour chaque entrée dans le BTB, le BTB mémorise le résultat des n dernières exécutions du branchement associé. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. Un tel historique est appelé un '''historique local'''. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout en transistor élevé, avec tous les défauts que ca implique en termes de consommation électrique et thermique. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées seules du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Il arrive que le circuit méta-prédicteur soit conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. Mais c'est loin d'être systématique. Une autre solution combine le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas, il y a un risque d'égalité. L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] Enfin, une dernière solution est que certaines unités de prédiction ont la priorité sur toutes les autres. Par exemple, prenons un processeur avec un ''return stack predictor'', une unité qui détecte les boucles et une unité de prédiction avec un historique. Si le ''return stack predictor'' détecte une instruction de retour de fonction, sa prédiction passera avant toutes les autres unités de prédiction. Et dans ce qui va suivre, c'est cette solution qui sera utilisé pour les unités de prédiction qu'on va étudier. ===La prédiction des branchements des boucles=== Les boucles sont à part, pour ce qui est de la prédiction de branchement. Rappelons qu'une boucle FOR/WHILE répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. La conséquence est que les unités de prédiction usuelles sont assez mauvaises pour prédire les branchements des boucles. Par contre, la prédiction statique marche particulièrement bien comparé au reste. La prédiction statique prédit que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Reste à gérer les branchements des boucles au mieux, en sachant cela. L'idée est d'utiliser plusieurs unités de prédiction séparées : une spécialisée dans les boucles et d'autres unités de prédiction. Uen unité de méta-prédiction se charge d'activer l'unité de prédiction des boucles dès qu'une boucle est détéctée. Les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ===Le prédicteur L-TAGE=== Il est possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Une méta-unité de branchement prédit alors quelle taille d'historique donne les meilleurs résultats. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. C'est d'ailleurs le principe derrière les '''unités de branchement TAGE (''TAgged GEometric length predictor'')'''. Elles utilisent plusieurs registres d'historiques, chacun avec sa propre PHT, dont la taille suit une suite géométrique. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. Le prédicteur TAGE a été raffiné avec le temps, sur plusieurs points, que je ne vais pas détailler dans ce qui suit. Précisons cependant que les PHT sont structurées en caches, afin d'éliminer les phénomènes d'''aliasing''. L'ajout d'une unité de prédiction pour les boucles, a donné le '''prédicteur L-TAGE''', qui a lui-même été amélioré par la suite. Les unités de branchement TAGE sont les plus utilisées au moment où j'écris ces lignes (2026), et la majorité des processeurs x86 commerciaux utilisent une variante de ce prédicteur, à quelques exceptions près. ===La prédiction de branchement à deux niveaux=== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> 1da8e5qzduooqijayezeqelrrmbu2y5 773888 773887 2026-10-01T17:16:58Z Mewtow 31375 /* La prédiction de branchement */ 773888 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris, et décide quelle adresse envoyer au cache en fonction du résultat. Si on prédit qu'un branchement est non pris, on laisse le ''program counter'' faire son travail. À l'inverse si le branchement est prédit comme étant pris, l'adresse de destination est envoyée au cahe d'instruction et au ''program counter''. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations assez diverses. Elle assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante, d'autant plus coûteuse que le pipeline est long. Il est généralement admis qu'il faut au minimum un taux de réussite proche de 90%, voire plus sur les processeurs modernes. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents. Ce registre d'historique est un registre à décalage mis à jour à chaque fois que le processeur exécute un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction. L'unité de prédiction globale utilise un compteur à saturation pour chaque valeur possible du registre d'historique. En clair, pour un registre de N bits, elle utilise 2^n compteurs à saturation. En clair, il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Avec un registre d'historique, la prédiction tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. L'unité de prédiction précédente est très simple et il existe des unités de prédiction similaires, basés sur des principes quelque peu différentes. Elles sont appelées des '''unités de prédiction à deux niveaux''', ce qui sous-entend que le registre d'historique et la PHT sont deux niveaux différents. Elles peuvent utiliser soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment, les unités de prédiction avec plusieurs PHTs seront vues dans la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] Dans l'unité de prédiction précédente, le choix du compteur à saturation utiliser se fait uniquement grâce au registre d'historique global. les unités que nous allons voir font un choix plus complexe, qui utilisent d'autres informations. Mais pourquoi utiliser d'autres informations ? Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Les unités de prédiction précédentes utilisaient une PHT globale, ce qui entrainait des problèmes d’''aliasing''. Une solution alternative utilise une PHT par branchement, tout en conservant un historique global. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. Chaque PHT associée à un branchement est appelée une '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] Au lieu d'un historique global, pour tous les branchements, il est possible d'utiliser un historique pour chaque branchement. Précisément, pour chaque entrée dans le BTB, le BTB mémorise le résultat des n dernières exécutions du branchement associé. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. Un tel historique est appelé un '''historique local'''. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout en transistor élevé, avec tous les défauts que ca implique en termes de consommation électrique et thermique. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées seules du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Il arrive que le circuit méta-prédicteur soit conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. Mais c'est loin d'être systématique. Une autre solution combine le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas, il y a un risque d'égalité. L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] Enfin, une dernière solution est que certaines unités de prédiction ont la priorité sur toutes les autres. Par exemple, prenons un processeur avec un ''return stack predictor'', une unité qui détecte les boucles et une unité de prédiction avec un historique. Si le ''return stack predictor'' détecte une instruction de retour de fonction, sa prédiction passera avant toutes les autres unités de prédiction. Et dans ce qui va suivre, c'est cette solution qui sera utilisé pour les unités de prédiction qu'on va étudier. ===La prédiction des branchements des boucles=== Les boucles sont à part, pour ce qui est de la prédiction de branchement. Rappelons qu'une boucle FOR/WHILE répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. La conséquence est que les unités de prédiction usuelles sont assez mauvaises pour prédire les branchements des boucles. Par contre, la prédiction statique marche particulièrement bien comparé au reste. La prédiction statique prédit que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Reste à gérer les branchements des boucles au mieux, en sachant cela. L'idée est d'utiliser plusieurs unités de prédiction séparées : une spécialisée dans les boucles et d'autres unités de prédiction. Uen unité de méta-prédiction se charge d'activer l'unité de prédiction des boucles dès qu'une boucle est détéctée. Les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ===Le prédicteur L-TAGE=== Il est possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Une méta-unité de branchement prédit alors quelle taille d'historique donne les meilleurs résultats. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. C'est d'ailleurs le principe derrière les '''unités de branchement TAGE (''TAgged GEometric length predictor'')'''. Elles utilisent plusieurs registres d'historiques, chacun avec sa propre PHT, dont la taille suit une suite géométrique. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. Le prédicteur TAGE a été raffiné avec le temps, sur plusieurs points, que je ne vais pas détailler dans ce qui suit. Précisons cependant que les PHT sont structurées en caches, afin d'éliminer les phénomènes d'''aliasing''. L'ajout d'une unité de prédiction pour les boucles, a donné le '''prédicteur L-TAGE''', qui a lui-même été amélioré par la suite. Les unités de branchement TAGE sont les plus utilisées au moment où j'écris ces lignes (2026), et la majorité des processeurs x86 commerciaux utilisent une variante de ce prédicteur, à quelques exceptions près. ===La prédiction de branchement à deux niveaux=== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> 8v5au1l0vvsndy6m36ss83tjllovtga 773889 773888 2026-10-01T17:17:39Z Mewtow 31375 /* La prédiction de branchement */ 773889 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris, et décide quelle adresse envoyer au cache en fonction du résultat. Si on prédit qu'un branchement est non pris, on laisse le ''program counter'' faire son travail. À l'inverse si le branchement est prédit comme étant pris, l'adresse de destination est envoyée au cahe d'instruction et au ''program counter''. La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante, d'autant plus coûteuse que le pipeline est long. Il est généralement admis qu'il faut au minimum un taux de réussite proche de 90%, voire plus sur les processeurs modernes. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== La prédiction de branchement assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations assez diverses. Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut aussi distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, avec la '''prédiction dynamique''', des informations qui ne sont disponibles qu'à l’exécution sont utilisées pour faire la prédiction. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Mais les techniques récentes effectuent des opérations statistiques assez complexes, comme nous le verrons plus bas. Pour ce qui est de la prédiction dynamique, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents. Ce registre d'historique est un registre à décalage mis à jour à chaque fois que le processeur exécute un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction. L'unité de prédiction globale utilise un compteur à saturation pour chaque valeur possible du registre d'historique. En clair, pour un registre de N bits, elle utilise 2^n compteurs à saturation. En clair, il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Avec un registre d'historique, la prédiction tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. L'unité de prédiction précédente est très simple et il existe des unités de prédiction similaires, basés sur des principes quelque peu différentes. Elles sont appelées des '''unités de prédiction à deux niveaux''', ce qui sous-entend que le registre d'historique et la PHT sont deux niveaux différents. Elles peuvent utiliser soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment, les unités de prédiction avec plusieurs PHTs seront vues dans la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] Dans l'unité de prédiction précédente, le choix du compteur à saturation utiliser se fait uniquement grâce au registre d'historique global. les unités que nous allons voir font un choix plus complexe, qui utilisent d'autres informations. Mais pourquoi utiliser d'autres informations ? Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Les unités de prédiction précédentes utilisaient une PHT globale, ce qui entrainait des problèmes d’''aliasing''. Une solution alternative utilise une PHT par branchement, tout en conservant un historique global. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. Chaque PHT associée à un branchement est appelée une '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] Au lieu d'un historique global, pour tous les branchements, il est possible d'utiliser un historique pour chaque branchement. Précisément, pour chaque entrée dans le BTB, le BTB mémorise le résultat des n dernières exécutions du branchement associé. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. Un tel historique est appelé un '''historique local'''. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout en transistor élevé, avec tous les défauts que ca implique en termes de consommation électrique et thermique. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées seules du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Il arrive que le circuit méta-prédicteur soit conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. Mais c'est loin d'être systématique. Une autre solution combine le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas, il y a un risque d'égalité. L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] Enfin, une dernière solution est que certaines unités de prédiction ont la priorité sur toutes les autres. Par exemple, prenons un processeur avec un ''return stack predictor'', une unité qui détecte les boucles et une unité de prédiction avec un historique. Si le ''return stack predictor'' détecte une instruction de retour de fonction, sa prédiction passera avant toutes les autres unités de prédiction. Et dans ce qui va suivre, c'est cette solution qui sera utilisé pour les unités de prédiction qu'on va étudier. ===La prédiction des branchements des boucles=== Les boucles sont à part, pour ce qui est de la prédiction de branchement. Rappelons qu'une boucle FOR/WHILE répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. La conséquence est que les unités de prédiction usuelles sont assez mauvaises pour prédire les branchements des boucles. Par contre, la prédiction statique marche particulièrement bien comparé au reste. La prédiction statique prédit que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Reste à gérer les branchements des boucles au mieux, en sachant cela. L'idée est d'utiliser plusieurs unités de prédiction séparées : une spécialisée dans les boucles et d'autres unités de prédiction. Uen unité de méta-prédiction se charge d'activer l'unité de prédiction des boucles dès qu'une boucle est détéctée. Les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ===Le prédicteur L-TAGE=== Il est possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Une méta-unité de branchement prédit alors quelle taille d'historique donne les meilleurs résultats. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. C'est d'ailleurs le principe derrière les '''unités de branchement TAGE (''TAgged GEometric length predictor'')'''. Elles utilisent plusieurs registres d'historiques, chacun avec sa propre PHT, dont la taille suit une suite géométrique. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. Le prédicteur TAGE a été raffiné avec le temps, sur plusieurs points, que je ne vais pas détailler dans ce qui suit. Précisons cependant que les PHT sont structurées en caches, afin d'éliminer les phénomènes d'''aliasing''. L'ajout d'une unité de prédiction pour les boucles, a donné le '''prédicteur L-TAGE''', qui a lui-même été amélioré par la suite. Les unités de branchement TAGE sont les plus utilisées au moment où j'écris ces lignes (2026), et la majorité des processeurs x86 commerciaux utilisent une variante de ce prédicteur, à quelques exceptions près. ===La prédiction de branchement à deux niveaux=== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> j8199tkh4z1s6hkxrww032x49cesdja 773890 773889 2026-10-01T17:19:42Z Mewtow 31375 /* La classification des techniques de prédiction de branchement */ 773890 wikitext text/x-wiki Les branchements sont pris en charge par plusieurs étages du pipeline. Exécuter un branchement demande de savoir deux choses : s'il est pris ou non, quelle est l'adresse de destination. L'adresse de destination est localisée grâce au mode d'adressage du branchement. Avec le mode d'adressage direct, elle est extraite de l'instruction par le séquenceur. Avec l'adressage indirect, elle est lue dans un registre. Avec les branchements relatifs, elle est calculée via un additionneur, qui additionne un décalage au ''program counter''. L'additionneur peut être soit dans l'étage de ''PC Update'', soit dans l'unité de calcul de l'étage d'exécution. Passons maintenant au résultat du branchement, à savoir s'il est pris ou non. Pour savoir si le branchement est pris, il faut distinguer les branchements conditionnels et inconditionnels. Pour rappel, les branchements inconditionnels regroupent les appels de fonction, les simples instructions de saut, et quelques autres. Ils sont toujours pris, ils s'exécutent toujours. A partir du moment où le décodeur a décodé un branchement inconditionnel, il sait que le branchement est pris. Pour les branchements conditionnels, ils sont pris seulement si une condition précise est respectée. On sait s'ils sont exécutés une fois que la condition est calculée par l'unité de calcul, ou par une unité de branchement spécialisée. [[File:Gestion des branchements sur un pipeline.png|centre|vignette|upright=2.0|Gestion des branchements conditionnels sur un pipeline.]] Sur le schéma précédent, les branchements inconditionnels ne sont pas montrés. Pour cela, il faudrait rajouter une connexion du décodeur vers le multiplexeur de branchement. De plus, il faudrait aussi rajouter de quoi retarder l'exécution du branchement seulement une fois que l'adresse est connue. Par exemple, elle est connue un cycle d'horloge plus si l'adresse est lue depuis les registres, comparé à un branchement direct. ==Les branchements et les exceptions imprécises/précises== Un branchement est censé altérer le ''program counter'' immédiatement, en un cycle d'horloge. Mais sur un pipeline, Le branchement fait bien reprendre le processeur au bon endroit, mais avec du retard. La raison est qu'un branchement met plusieurs cycles pour s'exécuter. Il faut plusieurs cycles pour déterminer quelle est l'adresse de destination, temps qui dépend de là où est lue l'adresse. Et on sait si le branchement est pris seulement au niveau du décodeur, voire au niveau de l'unité de calcul. Le branchement doit traverser plusieurs étages avant qu'on connaisse son résultat. Il y a bien 3 à 4 cycles avant cela. Et pendant ce temps de trajet , des instructions seront chargées dans le pipeline, potentiellement à tord. Si le branchement n'est pas pris, ce n'est pas grave : les instructions n'ont pas été chargées à tord. Mais dans le cas contraire, les instructions n'auraient pas du rentrer dans le pipeline. Le problème est similaire à celui observé avec les exceptions précises/imprécises. La différence est que les branchements sont des instructions machines, pas les exceptions matérielles. Mais le problème des instructions chargées à tord est le même dans les grandes lignes. Une différence est que les branchements sont résolus plus ou moins au même étage, alors que les exceptions peuvent survenir dans tout le pipeline. Une autre différence, bien plus importante, est que les branchements non-pris ne chargent pas d'instructions à tord et qu'il faut faire la différence entre branchement pris et non-pris. Dans la suite, nous parlerons d'instructions fautives pour parler des instructions chargées à tord. Pour corriger ce problème, il existe une solution logicielle. Le compilateur fait suivre les branchements par des instructions qui ne font rien, des NOP (No OPeration) : c'est ce qu'on appelle un '''délai de branchement'''. L'idée est que toutes les instructions chargées à tord sont des NOPs, qui ne font rien. Mais le nombre de NOP à ajouter dépend du pipeline du processeur, ce qui peut poser des problèmes de compatibilité. Mais d'autres solutions, matérielles cette fois, sont utilisées de nos jours. ===L'annulation des instructions au décodage=== Les branchements inconditionnels sont dans un cas plus favorable : ils sont identifiés dans l'unité de décodage et sont toujours pris. D'autres branchements sont dans ce cas : les appels de fonctions, les interruptions logicielles. Avec eux, le décodeur annule les instructions fautives, il les remplace par des instructions NOP. L'étage de décodage sait que les instructions suivant un branchement inconditionnel ont été chargées à tord. Précisément N instructions, avec N le nombre d'étages entre le chargement et le décodage (inclus). Les N instructions suivantes sont tout simplement annulés, elles sont remplacées par des NOPs. L'implémentation demande juste un simple compteur et quelques circuits annexes. Cependant, la technique ne marche pas du tout pour les branchements conditionnels. En effet, on ne sait pas s'ils sont pris ou non à l'étage de décodage. Du moins pas avec un pipeline normal. Mais certains processeur, basés sur le pipeline RISC classique, calculaient les branchements pendant l'étape de décodage, dans une ALU spécialisée qui travaille en parallèle de l'unité de décodage. Le défaut de cette technique est que l'ALU en question doit lire les registres. Pour rappel, sur ce pipeline, l'étape de décodage effectue la lecture des opérandes dans les registres, en parallèle de l'unité de décodage. Mais ce qu'on n'a pas dit dans le chapitre précédent, c'est qu'elle effectue aussi le calcul des branchements dans une mini-ALU séparée ! Ce faisant, le résultat du branchement est connu dès la fin de l'étape de décodage. ===L'usage du système d'exceptions précises pour les branchements conditionnels=== La solution gère le problème en empêchant les instructions chargées à tord d'enregistrer leurs résultat, afin qu'elles n'aient pas le moindre effet sur l'état du processeur. Le hardware pour les exceptions précises, décrit quelques paragraphes avant, est réutilisé pour cela. Les branchements conditionnels peuvent être pris ou non-pris et on doit tenir compte de ces deux cas. Si le branchement n'est pas pris, les instructions n'ont pas été chargées à tord, elles peuvent enregistrer leur résultat dans les registres. Par contre, ce n'est pas le cas pour les branchements pris, les mécanismes d'exceptions précise doivent alors d'activer. Pour cela, la meilleure solution est que l'unité de calcul génère un indicateur d'exception si le branchement n'est pas pris. L'ALU effectue la comparaison et génère ou non l'indicateur d'exception adéquat suivant le résultat de celle-ci. L'idée est d'utiliser un indicateur d'exception particulier, qui précise qu'un branchement a été pris, signe que des instructions ont été chargées à tord. L'indicateur est propagé dans le pipeline, jusqu'à atteindre l'étage d'enregistrement dans les registres. Cet étage décide s'il faut autoriser ou non les écritures qu'il recoit, pour annuler les instructions chargées à tord. Le ''program counter'' est corrigé immédiatement, simplement en exécutant le branchement. Reste à savoir qui génère cet indicateur d'exception de branchement pris. Diverses optimisations visent à éliminer les instructions chargées à tord, à faire en sorte que des instructions utiles soient chargées. Le chapitre suivant va détailler ces optimisations dans le détail, qui sont regroupées sous le terme de prédiction de branchement. Elles ne fonctionnent pas pour les exceptions précises, seulement les branchements. ===La prédiction de branchement=== Les techniques précédentes ont pour défaut que de nombreux cycles d'horloge sont gâchés à cause des instructions fautives. Pour éviter ce gâchis, les concepteurs de processeurs ont inventé l'''exécution spéculative de branchement'', qui consiste à deviner l'adresse de destination du branchement et charger les bonnes instructions dès le départ. Le but est de ne pas charger d'instruction à tord, en prédisant quelle seront les bonnes instructions à charger. Et cela demande de résoudre trois problèmes : * reconnaître les branchements ; * savoir si un branchement sera exécuté ou non : c'est la '''prédiction de branchement''' ; * dans le cas où un branchement serait exécuté, il faut aussi savoir quelle est l'adresse de destination : c'est la '''prédiction de l'adresse de destination''' d'un branchement, aussi appelée ''branch target prediction''. Pour résoudre le second problème, le processeur contient un circuit qui déterminer si le branchement est pris (on doit brancher vers l'adresse de destination) ou non pris (on poursuit l’exécution du programme immédiatement après le branchement) : c'est l''''unité de prédiction de branchement'''. La prédiction de direction de branchement est elle aussi déléguée à un circuit spécialisé : l’'''unité de prédiction de direction de branchement'''. C'est l'unité de prédiction de branchement qui autorise l'unité de prédiction de destination de branchement à modifier le ''program counter''. Dans certains processeurs, les deux unités sont regroupées dans le même circuit. [[File:Relation entre prédiction de branchement et de direction de branchement.png|centre|vignette|upright=2|Relation entre prédiction de branchement et de direction de branchement.]] ==Les erreurs de prédiction de branchement== Le processeur peut parfaitement se tromper en faisant ses prédictions, ce qui charge des instructions par erreur dans le pipeline. Dans ce cas, on parle d''''erreur de prédiction'''. Les erreurs de prédiction sont corrigées par le système d'exception précises, ou un équivalent. En clair, les instructions chargées à tord se propagent jusqu'à la sortie du pipeline, où leurs résultats sont alors annulés. N'importe quelle technique de gestion des exceptions précise fait le travail, mais les processeurs modernes utilisent un tampon de réordonnancement (''reorder buffer'') qu'on détaillera dans quelques chapitres. Détecter les erreurs de prédiction demande d'ajouter un circuit dans le processeur : l’''unité de vérification de branchement'' (''branch verification unit''). Elle génère un indicateur d'exception, qui est géré par le système d'exception précise. Pour générer l'indicateur, elle compare l'adresse de destination prédite et celle calculée en exécutant le branchement. Pour cela, l'adresse prédite doit être propagée dans le pipeline jusqu’à ce que l'adresse de destination du branchement soit calculée. [[File:Unité de vérification de branchement.png|centre|vignette|upright=2|Unité de vérification de branchement]] ===La pénalité de mauvaise prédiction=== Le temps pour vider le pipeline des instructions fautives dépend du pipeline. La dernière instruction à être chargée dans le pipeline le sera en même temps qu'on détecte l'erreur de prédiction. Il faudra attendre que cette instruction quitte le pipeline, et donc qu'elle traverse tous les étages. Il y a donc un temps d'attente pour collecter les résultats de ces instructions et la décision de ne pas enregistre leurs résultats dans les registres Le nombre d'instructions chargées à tord dépend de où est détectée l'erreur de prédiction. Plus le résultat du branchement est connu tard, plus le nombre d'instructions chargées à tord sera grand, et plus le nombre de cycles d'horloge gâchés le sera aussi. Le nombre de cycles gâchés s'appelle la '''pénalité de mauvaise prédiction'''. Elle correspond précisément au nombre de cycles/étages entre le premier étage du pipeline, et l'étage où le branchement est résolu (on connait l'adresse à laquelle brancher). Plus celui-ci est éloigné du début du pipeline, plus la pénalité est importante. Elle est généralement d'autant plus longue que le pipeline lui-même est long. Les concepteurs de processeurs font donc face à un compromis : un pipeline long permet d'exécuter plus d'instructions en parallèle, mais cela augmente aussi la pénalité de mauvaise prédiction. Un pipeline de 10-15 étapes est un bon compromis. Les pipelines plus longs ont besoin d'une prédiction de branchement élaborée pour fonctionner correctement. Plus la prédiction de branchement est performante, plus le pipeline peut être long. Les pénalités de mauvaise prédiction seront plus importantes, mais les mauvaises prédictions seront plus rares, ce qui compense complétement. ===La ''minimal control dependency''=== En utilisant une technique du nom de ''minimal control dependency'', seules les instructions qui dépendent du résultat du branchement sont supprimées du pipeline en cas de mauvaise prédiction, les autres n'étant pas annulées. Sans ''minimal control dependency'', toutes les instructions qui suivent un branchement dans notre pipeline sont annulées. Pourtant, certaines d'entre elles pourraient être utiles. Prenons un exemple : supposons que l'on dispose d'un processeur de 31 étages (un Pentium 4 par exemple). Supposons que l'adresse du branchement est connue au 9éme étage. On fait face à un branchement qui envoie le processeur seulement 6 instructions plus loin. Si toutes les instructions qui suivent le branchement sont supprimées, les instructions en rouge sont celles qui sont chargées incorrectement. On remarque pourtant que certaines instructions chargées sont potentiellement correctes : celles qui suivent le point d'arrivée du branchement. Elles ne le sont pas forcément : il se peut qu'elles aient des dépendances avec les instructions supprimées. Mais si elles n'en ont pas, alors ces instructions auraient dûes être exécutées. Il serait donc plus efficace de les laisser enregistrer leurs résultats au lieu de les ré-exécuter à l’identique un peu plus tard. Ce genre de choses est possible sur les processeurs qui implémentent une technique du nom de ''Minimal Control Dependancy''. En gros, cette technique fait en sorte que seules les instructions qui dépendent du résultat du branchement soient supprimées du pipeline en cas de mauvaise prédiction. [[File:Minimal cntrol dependency.png|centre|vignette|upright=2|Minimal control dependency]] ==La reconnaissance des branchements== Pour prédire des branchements, le processeur doit faire la différence entre branchements et autres instructions, dès l'étage de chargement. Or, un processeur « normal », sans prédiction de branchement, ne peut faire cette différence qu'à l'étage de décodage, et pas avant. Les chercheurs ont donc dû trouver une solution. La première solution se base sur les techniques de prédécodage vues dans le chapitre sur le cache. Pour rappel, ce prédécodage consiste à décoder partiellement les instructions lors de leur chargement dans le cache d'instructions et à mémoriser des informations utiles dans la ligne de cache. Dans le cas des branchements, les circuits de prédécodage peuvent identifier les branchements et mémoriser cette information dans la ligne de cache. Une autre solution consiste à mémoriser les branchements déjà rencontrés dans un cache intégré dans l'unité de chargement ou de prédiction de branchement. Ce cache mémorise l'adresse (le ''program counter'') des branchements déjà rencontrés : il s'appelle le tampon d’adresse de branchement (''branch adress buffer''). À chaque cycle d'horloge, l'unité de chargement envoie le ''program counter'' en entrée du cache. S’il n'y a pas de défaut de cache, l'instruction à charger est un branchement déjà rencontré : on peut alors effectuer la prédiction de branchement. Dans le cas contraire, la prédiction de branchement n'est pas utilisée, et l'instruction est chargée normalement. ==La prédiction de l'adresse de destination== Les explications qui vont suivre vont faire intervenir deux adresses. La première est l''''adresse de destination''' du branchement, à savoir l'adresse à laquelle le processeur doit reprendre son exécution si le branchement est pris. L'autre adresse est l''''adresse du branchement''' lui-même, l'adresse qui indique la position de l'instruction de branchement en mémoire. L'adresse du branchement est contenu dans le ''program counter'' lorsque celui-ci est chargé dans le processeur, alors que l'adresse de destination est fournie par l'unité de décodage d'instruction. Il faudra faire bien attention à ne pas confondre les deux adresses dans ce qui suit. La '''prédiction de l'adresse de destination d'un branchement''' détermine l'adresse de destination d'un branchement. Rappelons qu'il existe plusieurs modes d'adressage pour les branchements et que chacun d'entre eux précise l'adresse de destination à sa manière. Suivant le mode d'adressage, l'adresse de destination est soit dans l'instruction elle-même (adressage direct), soit calculée à l’exécution en additionnant un ''offset'' au ''program counter'' (adressage relatif), soit dans un registre du processeur (branchement indirect), soit précisée de manière implicite (retour de fonction, adresse au sommet de la pile). La prédiction des branchements directs et relatifs se fait globalement de la même manière, ce qui fait que nous ferons la confusion dans ce qui suit. Pour les branchements implicites, ils correspondent presque exclusivement aux instructions de retour de fonction, qui sont un cas un peu à part que nous verrons dans la section sur les branchements indirects. Pour résumer, nous allons faire une différence entre les branchements directs pour lequel l'adresse de destination est toujours la même, les branchements indirects où l'adresse de destination est variable durant l’exécution du programme, et les instructions de retour de fonction. Les branchements directs sont facilement prévisibles, vu que l'adresse vers laquelle il faut brancher est toujours la même. Pour les branchements indirects, vu que cette adresse change, la prédire celle-ci est particulièrement compliqué (quand c'est possible). Pour les instructions de retour de fonction, une prédiction parfaite est possible. ===La prédiction de l'adresse de destination pour les branchements directs=== Lorsqu'un branchement est exécuté, on peut se souvenir de l'adresse de destination et la réutiliser lors d'exécutions ultérieures du branchement. Cette technique marche à la perfection pour les branchements directs, pour lesquels cette adresse est toujours la même, mais pas pour les branchements indirects. Pour se souvenir de l'adresse de destination, on utilise un cache qui mémorise les correspondances entre l'adresse du branchement et l'adresse de destination. Il est appelé le '''tampon de destination de branchement''', ''branch target buffer'' en anglais, qui sera abrévié en BTB dans ce qui suit. Le BTB est une amélioration du tampon d'adresse de branchement vu plus haut, auquel on aurait ajouté les adresses de destination des branchements. Le BTB est utilisé comme suit : on envoie en entrée l'adresse du branchement lors du chargement, le BTB répond au cycle suivant en précisant : s’il reconnaît l'adresse d'entrée, et quelle est l'adresse de destination si c'est le cas. Précisons que le BTB ne mémorise pas les branchements non-pris, ce qui est inutile. Le BTB peut être un cache totalement associatif, associatif par voie, ou directement adressé, mais ces derniers sont les plus fréquents. Les BTB sont généralement des caches de type directement adressés, où seuls les bits de poids faible de l'adresse du branchement adressent le cache, le reste de l'adresse est tout simplement ignoré. Il existe aussi des BTB qui sont construits comme les caches associatifs à plusieurs voies, mais ceux-ci impliquent généralement la présence de ''tags'', ce qui fait qu'ils sont assez rares. [[File:Branch target buffer directement adressé.png|centre|vignette|upright=2|Branch target buffer directement adressé.]] Comme pour tous les caches, un accès au BTB peut entraîner un défaut de cache, c’est-à-dire qu'un branchement censé être dans le BTB n'y est pas. Comme pour les autres caches, les défauts de cache peuvent se classer en trois types : les défauts de cache à froid (''cold miss'') causés par la première exécution d'un branchement, les défauts liés à la capacité du BTB/cache, et les défauts par conflit d'accès au cache. Les défauts liés à la capacité du BTB sont les plus simples à comprendre. La capacité limitée du BTB fait que d'anciens branchements sont éliminées pour laisser la place à de nouveaux, généralement en utilisant algorithme de remplacement de type LRU. En conséquence, certains branchements peuvent donner des erreurs de prédiction si leur correspondance a été éliminée du cache. On peut en réduire le nombre en augmentant la taille du BTB. Les défauts liés aux conflits d'accès au BTB sont eux beaucoup plus intéressants, car la conception du BTB est assez spéciale dans la manière dont sont gérés ce genre de défauts. Le BTB est un cache partiellement associatif, ce qui fait que des défauts de cache par conflit d'accès peuvent avoir lieu. Pour rappel, les défauts par conflit d'accès ont lieu quand deux adresses mémoires se voient attribuer la même ligne de cache. Pour un BTB, cela correspond au cas où deux branchements se voient attribuer la même entrée dans le BTB, ce qui porte le nom d’''aliasing''. Ne pas détecter ce genre de situation sur un cache normal entraînerait des problèmes : la donnée lue/écrite ne serait pas forcément la bonne. Les caches normaux utilisent un système de tags pour éviter ce problème et on s'attendrait à ce que les BTB fassent de même. S'il existe des BTB qui utilisent des ''tags'', ils sont cependant assez rares. Pour un BTB, l'absence de ''tags'' n'est pas un problème : la seule conséquence est une augmentation du taux de mauvaises prédictions, dont les conséquences en termes de performance peuvent être facilement compensées. Les BTB se passent généralement de tags, ce qui a de nombreux avantages : le circuit est plus rapide, prend moins de portes logiques, consomme moins d'énergie, etc. Ces économies de portes logiques et de performances peuvent être utilisées pour augmenter la taille du BTB, ce qui compense, voire surcompense les mauvaises prédictions induites par l’''aliasing''. Des processeurs se passent de BTB. À la place, ils mémorisent les correspondances entre branchement et adresse de destination dans les bits de contrôle du cache d'instructions. Cela demande de mémoriser trois paramètres : l'adresse du branchement dans la ligne de cache (stockée dans le ''branch bloc index''), l'adresse de la ligne de cache de destination, la position de l'instruction de destination dans la ligne de cache de destination. Lors de l'initialisation de la ligne de cache, on considère qu'il n'y a pas de branchement dedans. Les informations de prédiction de branchement sont ensuite mises à jour progressivement, au fur et à mesure de l’exécution de branchements. Le processeur doit en même temps charger l'instruction de destination correcte dans le cache, si un défaut de cache a lieu : il faut donc utiliser un cache multi-port. L'avantage de cette technique est que l'on peut mémoriser une information par ligne de cache, comparé à une instruction par entrée dans un tampon de destination de branchement. Mais cela ralentit fortement l'accès au cache et gaspille du circuit (les bits de contrôle ajoutés ne sont pas gratuits). En pratique, on n'utilise pas cette technique, sauf sur quelques processeurs (un des processeurs Alpha utilisait cette méthode). ===La prédiction de l'adresse de destination pour les branchements indirects=== Passons maintenant aux branchements indirects. Les techniques précédentes fonctionnement quand on les applique aux branchements indirects et ne marchent pas trop mal, mais elles se trompent à chaque fois qu'un branchement indirect change d'adresse de destination. Tous les processeurs commerciaux datant d'avant le Pentium M sont dans ce cas. De nos jours, les processeurs haute performance sont capables de prédire l'adresse de destination d'un branchement indirect. Mais même malgré ces techniques avancées de prédiction, les branchements indirects et appels de sous-programmes indirects sont souvent très mal prédits. Les processeurs modernes utilisent souvent une '''unité de prédiction des branchements indirects''', séparée de l'unité de prédiction de branchement normale. Elle utilise un BTB amélioré, qui mémorise plusieurs adresses de destination pour un seul branchement, en compagnie d'informations pour prédire laquelle est la bonne. Il est possible d'utiliser un BTB unique, pour tous types de branchements, appelé un '''BTB unifié'''. Cette solution est illustrée ci-dessous, avec un tableau. Mais elle est tout sauf optimale, et n'est presque jamais utilisée en pratique. {|class="wikitable" |+ Branch Taarget Buffer unifié |- ! Adresse du branchement !! Informations de prédiction !! Adresse de destination !! Seconde adresse de destination |- | 0x54462146 || 11101011 || 0x52495674 || 0x24987461 |- | 0x54142446 || 11101101 || 0x15646956 || x0x4158746 |- | 0x25456542 || 10011101 || 0x52495674 || 0x24157461 |- | 0x15455855 || 11010101 || 0x52485754 || 0x24024511 |- | ... || ... || ... || ... |- | ... || ... || ... || ... |- | 0x54462146 || 1001101 || 0x52495674 || 0x24987461 |} Une seconde solution utilise un BTB séparé, distinct du BTB pour les branchements directs. En séparant les deux, on a un BTB simple qui ne mémorise qu'une adresse de destination par branchement, et un BTB pour branchements indirects avec plusieurs adresses de destination par branchement. [[File:BTB avec prédiction de branchements indirects.png|centre|vignette|upright=2.5|BTB avec prédiction de branchements indirects.]] L'idée est que l'on économise des circuits à nombre d'entrées égales. Par exemple, comparons ce que cela donne avec 64 entrées. D'un côté, on a un BTB à 64 entrées, capable de gérer deux adresse de destination par branchement. En pratique, la majorité des branchements sont des branchements directs, qui n'utiliseraient qu'une seule case mémoire. Les circuits du BTB seraient donc sous-utilisés. De l'autre, on a un BTB à 48 entrées pour branchements directs, et un second BTB avec 16 entrées, capable de mémoriser deux adresses par branchements. Le premier BTB utilise moins de circuits, et est utilisé à pleine capacité, il ne gaspille pas de circuits pour mémoriser des adresses de destination vides. Par contre, on perd un peu en flexibilité. L'organisation précédente marche bien si 1/4 des branchements rencontrés sont des branchements indirects. Mais si le ratio est différent, la performance est légèrement sous-optimale. Attention, cela ne veut pas dire qu'il y aura des entrés vides dans un BTB, non. Simplement, le premier BTB risque de manquer de place pour les branchements directs récents, alors que les branchements indirects seront eux bien prédits, même les anciens. A l'opposé, un BTB unique permet de mémoriser les 64 derniers branchements, peu importe qu'ils soient indirects ou non. En clair : un BTB unique est plus flexible, ce qui donne un gain de performance dans certaines situations. Cependant, cet avantage théorique ne tient pas compte de l'économie en circuits, qui peut être utilisée pour faire grossir le BTB pour branchements directs. Par exemple, à budget à transistor équivalent, on a le choix entre BTB à 64 entrées uniques, et un BTB pour branchements directs de 80 entrées avec un BTB indirect de 16 entrées. Le fait qu'un BTB soit plus gros permet des gains de performances qui compensent la perte de flexibilité. On peut ainsi avoir le meilleur des deux monde, à savoir une économie de circuits et les gains en performance qui en découlent, malgré une petite perte de flexibilité. ===La prédiction de l'adresse de retour d'une fonction=== Presque tous les processeurs conçus après les années 2000 peuvent prévoir l'adresse à laquelle il faudra reprendre lorsqu'une fonction/sous-programme a fini de s’exécuter. Ils font de la '''prédiction de retour de fonction'''. Pour cela, ils possèdent pour une unité de prédiction de branchement spécialisée, appelée le ''return stack predictor'', ou '''''pile de prédiction de retour de fonction'''''. Le terme ''return stack predictor'' étant plus court, nous l'utiliserons dans la suite du cours. Le ''return stack predictor'' est capable de reconnaitre les instructions de retour de fonction, et de prédire l'adresse de retour. Pour comprendre comment elle fait, rappelez-vous le chapitre sur la pile d'appel. Dans ce chapitre, j'avais introduit le concept de ''pile d'adresse de retour'', à savoir une pile d'appel très simplifiée, qui ne contient que les adresses de retour des fonctions en cours d'exécution. Le ''return stack predictor'' est essentiellement composé d'une telle pile d'adresse de retour, avec des circuits pour empiler et dépiler les adresses de retour. Le ''return stack predictor'' doit reconstruire une pile d'adresses de retour, à partir du programme exécuté. La pile d'adresse est mémorisée dans le ''return stack predictor'', elle est totalement séparée de la pile d'appel normale. Le programme exécuté continue d'utiliser la même pile d'appel qu'avant, ce qui fait que les adresses de retour sont dispersées dans la pile d'appel, parfois dans des registres spéciaux du processeur. Mais cela ne pose pas de problèmes pour le processeur, qui reconstruit la pile d'adresse de retour avec l'aide du décodeur. La reconstruction se fait à partir des instructions d'appel et de retour de fonction. Une instruction d'appel de fonction empile une adresse de retour, une instruction de retour de fonction la dépile. Lorsque le décodeur d'instruction reconnait une instruction d'appel de fonction, il extrait l'adresse de retour et l'envoie au ''return stack predictor''. L'adresse de retour est alors empilée, ce qui fait qu'elle se retrouve au sommet de la pile d'adresse de retour. Dépiler une adresse de retour revient à prédire un branchement. Lorsque le ''return stack predictor'' détecte une instruction de retour de fonction, il dépile l'adresse au sommet de la pile. L'adresse est alors envoyée du cache d'instruction, pour l'ire l'instruction adéquate, et le ''program counter'' est mis à jour avec cette adresse. Toute la difficulté revient à détecter les instructions de retour de fonction, sans quoi on ne sait pas quand dépiler l'adresse. La documentation là-dessus n'est pas très fournie, il est difficile de savoir ce qui est fait dans les processeurs modernes. Mais on peut faire quelques suppositions. La première possibilité est l'usage du ''prédécodage'', une technique qui décode partiellement les instructions lors de leur chargement dans le cache d'instruction. Elle sera vue dans le chapitre suivant, mais parlons-en rapidement. Le prédécodage ajoute quelques bits au début de chaque instruction pour préciser si c'est un branchement conditionnel, inconditionnel, un retour de fonction. On peut ainsi savoir rapidement quand dépiler : dès qu'une instruction est lue dans le cache d'instruction, le ''return stack predictor'' vérifie les bits ajoutés et agit en fonction. La seconde possibilité est que le BTB reconnait l'instruction de retour de fonction. Pour chaque branchement, il mémorise non seulement son adresse de retour, mais aussi son type. Par type, on veut dire : si c'est un branchement conditionnel, inconditionnel, un retour de fonction. Les retours de fonction sont donc reconnus à condition qu'ils aient été exécutés auparavant. Il est aussi possible d'avoir un BTB spécialisé dans les retours de fonction, qui est alimenté par les décodeurs. Dès que le décodeur reconnait une instruction de retour de fonction, il ajoute son ''program counter'' dans ce BTB spécialisé. Une dernière possibilité est que le ''return stack predictor'' agit une fois l'instruction partiellement décodée. Le décodeur d'instruction reconnait les instructions de retour de fonction rapidement, en un cycle, et consulte alors le ''return stack predictor''. L'adresse est dépilée, et le décodeur sait où brancher. Il n'a pas besoin de devoir lire l'adresse de retour en mémoire, il la lit depuis le ''return stack predictor'', directement. La prédiction se fait donc assez tard dans le pipeline, quelques instructions ont été chargées à tord. Mais le décodeur peut les annulers, comme il le ferait avec un branchement inconditionnel mal prédit. ==La prédiction de branchement== La prédiction de branchement tente de prédire si un branchement sera pris ou non-pris, et décide quelle adresse envoyer au cache en fonction du résultat. Si on prédit qu'un branchement est non pris, on laisse le ''program counter'' faire son travail. À l'inverse si le branchement est prédit comme étant pris, l'adresse de destination est envoyée au cahe d'instruction et au ''program counter''. La prédiction de branchement n'a d'intérêt que si les prédictions sont suffisamment fiables pour valoir le coup. Rappelons que la pénalité lors d'une mauvaise prédiction est importante, d'autant plus coûteuse que le pipeline est long. Il est généralement admis qu'il faut au minimum un taux de réussite proche de 90%, voire plus sur les processeurs modernes. Heureusement, la plupart des branchements sont des '''branchements biaisés''', c’est-à-dire qu'ils sont presque toujours pris ou presque toujours non-pris, sauf en de rares occasions. De tels branchements font que la prédiction des branchements est facile, bien qu'ils posent paradoxalement quelques problèmes, comme nous le verrons plus bas. Un autre point important est que les unités de prédiction de branchement doivent être très rapides. Idéalement, elles doivent fournir leur prédiction en un seul cycle d'horloge. Un seul cycle d'horloge est un temps très court, surtout sur les ordinateurs avec une fréquence élevée. Les techniques de prédiction dynamique ne peuvent donc pas utiliser des méthodes statistiques extraordinairement complexes. Cela va à l'encontre du fait que le taux de mauvaises prédictions doit être très faible, idéalement inférieur à 1% sur les processeurs modernes. Vous comprenez donc aisément que concevoir une unité de prédiction de branchement est un véritable défi d’ingénierie électronique qui requiert des prouesses technologiques sans précédent. ===La classification des techniques de prédiction de branchement=== La prédiction de branchement assigne une probabilité qu'un soit branchement soit pris en fonction de ces informations : si la probabilité est de plus de 50%, le branchement est considéré comme pris. La prédiction de branchement se base avant tout sur des statistiques, c’est-à-dire qu'elle détermine la probabilité d’exécution d'un branchement en fonction d'informations assez diverses. Suivant la nature des informations utilisées, on peut distinguer plusieurs types de prédiction de branchement : locale, globale, dynamique, statique, etc. Il faut d'abord distinguer la prédiction de branchements statique de la prédiction dynamique. Avec la '''prédiction statique''', on prédit si le branchement est pris ou non en fonction de ses caractéristiques propres, comme son adresse de destination, la position du branchement en mémoire, le mode d'adressage utilisé, si le branchement est direct ou indirect, ou toute autre information encodée dans le branchement lui-même. Les informations utilisées sont disponibles dans le programme exécuté, et ne dépendent pas de ce qui se passe lors de l’exécution du programme en lui-même. Par contre, la '''prédiction dynamique''' utilise des informations qui ne sont disponibles qu'à l’exécution. Typiquement, la prédiction dynamique extrait des statistiques sur l’exécution des branchements, qui sont utilisées pour faire la prédiction. Les statistiques en question peuvent être très simples : une simple moyenne sur les exécutions précédentes donne déjà de bons résultats. Suivant les informations utilisées, il faut distinguer la prédiction de branchement dynamique locale et globale. La '''prédiction locale''' sépare les informations de chaque branchement, alors que la '''prédiction globale''' fusionne les informations pour tous les branchements et fait des moyennes globales. Les deux méthodes ont leurs avantages et leurs inconvénients, car elles n'utilisent pas les mêmes informations. La prédiction locale seule ne permet pas d'exploiter les corrélations entre branchements, à savoir que le résultat d'un branchement dépend souvent du résultat des autres, alors que la prédiction globale le peut. À l'inverse, la prédiction globale seule n'exploite pas des informations statistiques précise pour chaque branchement. Idéalement, les deux méthodes sont complémentaires, ce qui fait qu'il existe des prédicteurs de branchement hybrides, qui exploitent à la fois les statistiques pour chaque branchement et les corrélations entre branchements. Les prédicteurs hybrides ont de meilleures performances que les prédicteurs purement globaux ou locaux. ===La prédiction statique de branchement=== Avec la '''prédiction statique''', on prédit le résultat du branchement en fonction de certaines caractéristiques du branchement lui-même, comme son adresse, son mode d'adressage, l'adresse de destination connue, etc. Dans son implémentation la plus explicite, la prédiction est inscrite dans le branchement lui-même. Quelques bits de l'opcode du branchement précisent si le branchement est majoritairement pris ou non pris. Ils permettent d'influencer les règles de prédiction de branchement et de passer outre les réglages par défaut. Ces bits sont appelés des '''suggestions de branchement''' (''branch hint''). Mais tous les processeurs ne gèrent pas cette fonctionnalité. Et de plus, cette solution marche assez mal. La raison est que le programmeur ou le compilateur doit déterminer si le branchement est souvent pris ou non-pris, mais qu'il n'a pas de moyen réellement fiable pour cela. Une solution possible est d’exécuter le programme sur un ensemble de données réalistes et d'analyser le comportement de chaque branchement, afin de déterminer les suggestions de branchement adéquates, mais c'est une solution lourde et peu pratique, qui a de bonnes chances de donner des résultats peu reproductibles d'une exécution à l'autre. Une autre idée part de la distinction entre les branchements inconditionnels toujours pris, et les branchements conditionnels au résultat variable. Ainsi, on peut donner un premier algorithme de prédiction statique : les branchements inconditionnels sont toujours pris alors que les branchements conditionnels ne sont jamais pris (ce qui est une approximation). Cette méthode est particulièrement inefficace pour les branchements de boucle, où la condition est toujours vraie, sauf en sortie de boucle ! Il a donc fallu affiner légèrement l'algorithme de prédiction statique. Une autre manière d’implémenter la prédiction statique de branchement est de faire une différence entre les branchements conditionnels ascendants et les branchements conditionnels descendants. Un branchement conditionnel ascendant est un branchement qui demande au processeur de reprendre plus loin dans la mémoire : l'adresse de destination est supérieure à l'adresse du branchement. Un branchement conditionnel descendant a une adresse de destination inférieure à l'adresse du branchement : le branchement demande au processeur de reprendre plus loin dans la mémoire. Les branchements ascendants sont rarement pris (ils servent dans les conditions de type SI…ALORS), contrairement aux branchements descendants (qui servent à fabriquer des boucles). On peut ainsi modifier l’algorithme de prédiction statique comme suit : * les branchements inconditionnels sont toujours pris ; * les branchements descendants sont toujours pris ; * les branchements ascendants ne sont jamais pris. ===La prédiction de branchement avec des compteurs à saturation=== Les méthodes de prédiction de branchement statique sont intéressantes, mais elles montrent rapidement leurs limites. La prédiction dynamique de branchement donne de meilleures résultats. Sa version la plus simple se contente de mémoriser ce qui s'est passé lors de l’exécution précédente du branchement. Si le branchement a été pris, alors on suppose qu'il sera pris la prochaine fois. De même, s'il n'a pas été pris, on suppose qu'il ne le sera pas lors de la prochaine exécution. Pour cela, chaque entrée du BTB est associée à un bit qui mémorise le résultat de la dernière exécution du branchement : 1 s'il a été pris, 0 s'il n'a pas été pris. C'est ce qu'on appelle la '''prédiction de branchements sur 1 bit'''. Cette méthode marche bien pour les branchements des boucles (pas ceux dans la boucle, mais ceux qui font se répéter les instructions de la boucle), ainsi que pour les branchements inconditionnels, mais elle échoue assez souvent pour le reste. Sa performance est généralement assez faible, malgré son avantage pour les boucles. Il faut dire que les pénalités en cas de mauvaise prédiction sont telles qu'une unité de prédiction de branchement doit avoir un taux de succès très élevé pour être utilisable ne pratique, et ce n'est pas le cas de cette technique. Une version améliorée calcule, pour chaque branchement, une moyenne des exécutions précédentes. Typiquement, on mémorise à chaque exécution du branchement si celui-ci est pris ou pas, et on effectue une moyenne statistique sur toutes les exécutions précédentes du branchement. Si la probabilité d'être pris est supérieure à 50 %, le branchement est considéré comme pris. Dans le cas contraire, il est considéré comme non pris. Les premiers processeurs PA RISC mémorisaient, pour chaque branchement, les trois dernières exécution du branchement. Ils utilisaient pour cela un registre de trois bits, pour chaque entrée du BTB. Le bit était mis à 1 si le branchement avait été pris, à 0 s'il était non-pris. La prédiction de branchement était réalisée en utilisant une porte à majorité, qui prenait les trois bits et déterminait le bit majoritaire. Ce dernier était alors utilisé comme prédiction : le branchement sera pris s'il vaut 1, non-pris s'il vaut 0. Mettre à jour le registre était très simple, celui-ci était un simple registre à décalage. Une solution alternative, plus fréquemment utilisée, utilise un '''compteur à saturation'''. Elle ne mémorise pas chaque exécution précédente du branchement. A la place, elle utilise un compteur qui est incrémenté à chaque fois qu'un branchement est pris, décrémenté s'il est non-pris. Le compteur est initialisé de manière à avoir une probabilité proche de 50%, ce qui fait qu'il est initialisé à la moitié de sa valeur maximale. Pour faire la prédiction, on regarde le bit de poids fort du compteur : le branchement est considéré comme pris si ce bit de poids fort est à 1, et non pris s'il vaut 0. En clair : on considère que le branchement est pris si le compteur indique une probabilité supérieure à 50%. [[File:Compteur à saturation.png|centre|vignette|upright=2|Compteur à saturation.]] : Pour la culture générale, il faut savoir que le compteur à saturation du Pentium 1 était légèrement bogué. La plupart des processeurs utilisent un compteur à saturation par branchement, ce qui veut dire un compteur à saturation par entrée dans le BTB. Vu chaque compteur est associé à une entrée du BTB, le phénomène d’''aliasing'' mentionné plus haut peut perturber les prédictions de branchement. Concrètement, deux branchements peuvent se voir associés à la même entrée du BTB, et agir sur le même compteur à saturation, ce qui entraine l'apparition d''''interférences entre branchements'''. L'interférence peut avoir des effets positifs, neutres ou négatifs, suivant la corrélation entre les deux branchements. L’''aliasing'' n'est pas un problème si les deux branchements sont fortement corrélés, c’est-à-dire que les deux sont souvent pris ou au contraire que les deux sont très souvent non-pris. Dans ce cas, les deux branchements vont dans le même sens et l'interférence est neutre, voire légèrement positive. Mais si l'un est souvent pris et l’autre souvent non-pris, alors l'interférence est négative. En conséquence, les deux branchements vont se marcher dessus sans vergogne, chacun interférant sur les prédictions de l'autre ! Il est rare que l’''aliasing'' ait un impact significatif sur les unités de prédiction des deux paragraphes précédents. Il se manifeste surtout quand la BTB est très petite et la seule solution est d'augmenter la taille du BTB. ===La prédiction de branchement à deux niveaux avec un historique global=== La prédiction par historique conserve le résultat des branchements précédents dans un '''registre d'historique''', qui mémorise le résultat des N branchements précédents. Ce registre d'historique est un registre à décalage mis à jour à chaque fois que le processeur exécute un branchement : on fait rentrer un 1 si le branchement est pris, et un 0 sinon. Une unité de prédiction globale utilise cet historique pour faire sa prédiction. L'unité de prédiction globale utilise un compteur à saturation pour chaque valeur possible du registre d'historique. En clair, pour un registre de N bits, elle utilise 2^n compteurs à saturation. En clair, il n'y a pas un ou plusieurs compteurs à saturation par branchement, l'ensemble est organisé différemment. Les compteurs à saturation sont regroupés dans une '''''Pattern History Table''''', que nous noterons PHT dans ce qui suit. [[File:Prédiction dynamique à deux niveaux.png|centre|vignette|upright=2|Prédiction dynamique à deux niveaux.]] Avec un registre d'historique, la prédiction tient compte d'éventuelles corrélations entre branchements consécutifs/proches pour faire les prédictions. C'est un avantage pour les applications qui enchaînent des if...else ou qui contiennent beaucoup de if...else imbriqués les uns dans les autres, qui s’exécutent plusieurs fois de suite. Le résultat de chaque if...else dépend généralement des précédents, ce qui rend l'utilisation d'un historique global intéressant. Par contre, ils sont assez mauvais pour le reste des branchements. Mais cette qualité devient un défaut quand elle détecte des corrélations fortuites ou parasites, inutiles pour la prédiction (corrélation n'est pas causalité, même pour prédire des branchements). Du fait de ce défaut, la prédiction globale a besoin de registres d'historiques globaux très larges, de plusieurs centaines de bits au mieux. L'unité de prédiction précédente est très simple et il existe des unités de prédiction similaires, basés sur des principes quelque peu différentes. Elles sont appelées des '''unités de prédiction à deux niveaux''', ce qui sous-entend que le registre d'historique et la PHT sont deux niveaux différents. Elles peuvent utiliser soit une PHT unique, appelée '''PHT globale''', où une PHT associée chaque branchement, ce qui s'appelle une '''PHT locale'''. Mais laissons ce côté ce détail pour le moment, les unités de prédiction avec plusieurs PHTs seront vues dans la suite. Pour le moment, nous allons nous concentrer sur les unités avec une seule PHT couplé à un seul registre d'historique. [[File:2 level branch predictor.svg|centre|vignette|upright=2|Unités de prédiction de branchement à deux niveaux avec une seule PHT.]] Dans l'unité de prédiction précédente, le choix du compteur à saturation utiliser se fait uniquement grâce au registre d'historique global. les unités que nous allons voir font un choix plus complexe, qui utilisent d'autres informations. Mais pourquoi utiliser d'autres informations ? Le circuit précédent a cependant un problème : si deux branchements différents s'exécutent et que l'historique est le même, le processeur n'y verra que du feu. Il y a alors une interférence, à savoir que les deux branchements vont agir sur le même compteur à saturation. On se retrouve alors dans une situation d’''aliasing'', conceptuellement identique à l’''aliasing'' dans le BTB ou avec des compteurs à saturation. Sauf que les interférences sont alors beaucoup plus nombreuses et qu'elles sont clairement un problème pour les performances ! Et en réduire le nombre devient alors un problème bien plus important qu'avec les unités de prédiction précédentes. Si réduire l'''aliasing'' avec de simples compteurs à saturation demandait d'augmenter la taille de la PHT, d'autres solutions sont possibles sur les unités de prédiction globales. Elles sont au nombre de deux : mitiger les interférences liées aux branchements biaisés, ajouter des informations qui discriminent les branchements. Voyons ces solutions dans le détail. ====La mitigation des interférences par usage de l'adresse de branchement==== La première solution pour réduire l'impact de l’''aliasing'' est d'augmenter la taille de la PHT et de l'historique. Plus l'historique et la PHT sont grands, moins l’''aliasing'' est fréquent. Mais avoir des historiques et des PHT de très grande taille n'est pas une mince affaire et le cout en termes de performances et de portes logiques est souvent trop lourd. Une autre solution consiste à utiliser, en plus de l'historique, des informations sur le branchement lui-même pour choisir le compteur à saturation à utiliser. Typiquement, on peut utiliser l'adresse du branchement en plus de l'historique, pour choisir le compteur à saturation adéquat. Pour être précis, on n'utilise que rarement l'adresse complète du branchement, mais seulement les bits de poids faible. La raison est que cela fait moins de bits à utiliser, donc moins de circuits et de meilleures performances. Mais cela fait que l’''aliasing'' est atténué, pas éliminé. Des branchements différents ont beau avoir des adresses différentes, les bits de poids faible de leurs adresses peuvent être identiques. Des confusions sont donc possibles, mais l'usage de l'adresse de branchement réduit cependant l’''aliasing'' suffisamment pour que cela suffise en pratique. Une unité de prédiction de ce type est illustrée ci-dessous. Sur cette unité de prédiction, le choix de la PHT est réalisé par l'historique, alors que le choix du compteur à saturation est réalisé par l'adresse du branchement. Pour concevoir le circuit, on part d'une unité de prédiction de branchement basée sur des compteurs à saturation, avec un compteur à saturation par branchement, sauf qu'on copie les compteurs à saturation en autant d'exemplaires que de valeurs possibles de l'historique. Par exemple, pour un historique de 4 bits, on a 16 valeurs différentes possibles pour l'historique, donc 16 PHT et donc 16 compteurs à saturation par branchement. Chaque compteur à saturation mémorise la probabilité que le branchement associé soit pris, mais seulement pour la valeur de l'historique associée. Le choix de la PHT utilisée pour la prédiction est réalisé par l'historique global, qui commande un multiplexeur relié aux PHT. Les compteurs à saturation d'un branchement sont mis à jour seulement quand le branchement s'est chargé pendant que l'historique associé était dans le registre d'historique. Cette méthode a cependant le défaut de gâcher énormément de transistors. [[File:Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des compteurs à saturation sélectionnés par l'historique global]] D'autres unités de prédiction fonctionnent sur le principe inverse de l'unité précédente. Sur les unités de prédiction qui vont suivre, le choix d'une PHT est réalisé par l'adresse du branchement, alors que le choix du compteur dans la PHT est réalisé par l'historique. C'est ce que font les unités de prédiction '''''gshare''''' et '''''gselect''''', ainsi que leurs dérivées. Avec les unités de prédiction '''''gselect''''', on concatène les bits de poids faible de l'adresse du branchement et l'historique pour obtenir le numéro du compteur à saturation à utiliser. Avec cette technique, on utilise une PHT unique, mais il est possible de découper celle-ci en plusieurs PHT. On peut par exemple utiliser une PHT par branchement/entrée du BTB. Ou alors, on peut utiliser une PHT apr valeur possible de l'historique, ce qui fait qu'on retrouve l'unité de prédiction précédente. [[File:Prédiction « gselect ».png|centre|vignette|upright=2|Prédiction « gselect ».]] Avec les unités de prédiction '''''gshare''''', on fait un XOR entre le registre d'historique et les bits de poids faible de l'adresse du branchement. Le résultat de ce XOR donne le numéro du compteur à utiliser. Avec cette technique, on utilise une PHT globale et un historique global, mais le registre d'historique est manipulé de manière à limiter l'''aliasing''. [[File:Prédiction « gshare ».png|centre|vignette|upright=2|Prédiction « gshare ».]] Intuitivement, on pourrait croire que les unités de prédiction gselect ont de meilleures performances. C'est vrai que les unités gshare combinent l'adresse du branchement et l'historique d'une manière qui fait perdre un peu d'information (impossible de retrouver l'adresse du branchement et l'historique à partir du résultat du XOR), alors que les unités gselect conservent ces informations. Mais il faut prendre en compte le fait que les deux unités doivent se comparer à PHT identique, de même taille. Prenons par exemple une PHT de 256 compteurs, adressée par 8 bits. Avec une unité gselect, les 8 bits sont utilisés à la fois pour l'historique et pour les bits de poids faible de l'adresse du branchement. Alors qu'avec une unité gshare, on peut utiliser un historique de 8 bits et les 8 bits de poids faible de l'adresse du branchement. L'augmentation de la taille de l'historique et du nombre de bits utilisés fait que l’''aliasing'' est réduit, et cela compense le fait que l'on fait un XOR au lieu d'une concaténation. ====La mitigation des interférences par la structuration en caches des PHTs==== Les techniques précédentes utilisent les bits de poids faible de l'adresse pour sélectionner la PHT adéquate, ou pour sélectionner le compteur dans la PHT. Mais le reste de l'adresse n'est pas utilisé. Cependant, il est théoriquement possible de conserver les bits de poids fort, afin d'identifier le branchement associé à un compteur. Pour cela, chaque compteur à saturation se voit associé à un ''tag'', comme pour les mémoires caches. Ce dernier mémorise le reste de l'adresse du branchement associé au compteur. Lorsque l'unité de prédiction démarre une prédiction, elle récupère les bits de poids fort de l'adresse du branchement et compare ceux-ci avec le ''tag''. S’il y a un ''match'', c'est signe que le compteur est bien associé au branchement à prédire. Dans le cas contraire, c'est signe qu'on fait face à une situation d’''aliasing'' et le compteur n'est pas utilisé pour la prédiction. Le résultat transforme la PHT en une sorte de cache assez particulier, qui associe un compteur à saturation à chaque couple adresse-historique. La technique du paragraphe précédent peut être encore améliorée afin de réduire l’''aliasing''. L’''aliasing'' sur une unité de prédiction de branchement est similaire aux conflits d'accès au cache, où deux données/adresses atterrissent dans la même ligne de cache. Une PHT peut formellement être vue comme un cache directement adressé. Une solution pour limiter ces conflits d'accès à de tels, est de les transformer en un cache associatif à n voies. On peut faire la même chose avec les unités de prédiction de branchement. On peut dupliquer les PHT, de manière à ce que les bits de poids faible de l'adresse se voient attribuer à plusieurs compteurs à saturation, un par branchement possible. Ce faisant, on réduit les interférences entre branchements. Mais cette technique augmente la quantité de circuits utilisés et la consommation d'énergie, ainsi que le temps d'accès, sans compter qu'elle requiert d'utilisation de ''tags'' pour fonctionner à la perfection. Pour les unités de prédiction de branchement, les mesures à ce sujet ne semblent pas montrer un impact réellement important sur les performances, comparé à une organisation de type directement adressé. Il est possible de pousser la logique encore plus loin en ajoutant des caches de victime et d'autres mécanismes courant sur les caches usuels. ====La mitigation des interférences liées aux branchements biaisés==== D'autres techniques limitent encore plus l’''aliasing'' en tenant compte de certains points liés au biais des branchements. L’''aliasing'' a un impact négatif quand deux branchements aliasés (qui sont attribués au même compteur à saturation) vont souvent dans des sens différents : si l'un est non-pris, l'autre a de bonnes chances d'être pris. De plus, la plupart des branchements sont biaisés (presque toujours pris ou presque toujours non-pris, à plus de 90/99% de chances) ou sont du moins très fortement orientés dans une direction qu'une autre. Plus un branchement a une probabilité élevée d' être pris ou non-pris, plus sa capacité à interférer avec les autres est forte. En conséquence, les branchements biaisés ou quasi biaisés sont les plus problématiques pour l’''aliasing''. Or, il est possible de concevoir des prédicteurs de branchements qui atténuent fortement les interférences des branchements biaisés ou quasi biaisés. C’est le cas de l’''agree predictor'' et de l'unité de prédiction bimodale, que nous allons voir dans ce qui suit. La '''prédiction par consensus''' (''agree predictor'') combine une unité de prédiction à historique global (idéalement de type gshare ou gselect, mais une unité normale marche aussi) et avec une unité de prédiction à un bit qui indique la direction privilégiée du branchement. L'unité de prédiction à un bit est en quelque sorte fusionnée avec le BTB. Le BTB contient, pour chaque branchement, un compteur à saturation de 1 bit qui indique si celui-ci est généralement pris ou non-pris. L'unité à historique global a un fonctionnement changé : elle calcule non pas la probabilité que le branchement soit pris ou non-pris, mais la probabilité que le résultat du branchement soit compatible avec le résultat de l'unité de prédiction de 1 bit. La prédiction finale vérifie que ces deux circuits sont d'accord, en faisant un NXOR entre les résultats des deux unités de prédiction de branchement. Des calculs simples de probabilités montrent que l'''agree predictor'' a des résultats assez importants. Ses résultats sont d'autant meilleurs que les branchements sont biaisés et penchent fortement vers le côté pris ou non-pris. [[File:Prédiction par consensus.png|centre|vignette|upright=2|Prédiction par consensus.]] L''''unité de prédiction bimodale''' part d'une unité gshare, mais sépare la PHT globale en deux : une PHT dédiée aux branchements qui sont très souvent pris et une autre pour les branchements très peu pris. Les branchements très souvent pris vont interférer très fortement avec les branchements très souvent non-pris, et inversement. Par contre, les branchements très souvent pris n'interféreront pas entre eux, de même que les branchements non-pris iront bien ensemble. D'où l'idée de séparer ces deux types de branchements dans des PHT séparées, pour éviter le mauvais ''aliasing''. Les deux PHT fournissent chacune une prédiction. La bonne prédiction est choisie par un multiplexeur commandé par une unité de sélection, qui se charge de déduire quelle unité a raison. Cette unité de sélection est une unité de prédiction basée sur des compteurs à saturation de 2bits. On peut encore améliorer l'unité de sélection de prédiction en n'utilisant non pas des compteurs à saturation, mais une unité de prédiction dynamique à deux niveaux, ou toute autre unité vue auparavant. [[File:Prédiction bimodale.jpg|centre|vignette|upright=2|Prédiction bimodale.]] ===Les unités de prédiction à deux niveaux locales=== Les unités de prédiction précédentes utilisaient une PHT globale, ce qui entrainait des problèmes d’''aliasing''. Une solution alternative utilise une PHT par branchement, tout en conservant un historique global. De telles unités fonctionnent comme suit : on choisit une PHT en fonction du branchement, puis le compteur à saturation est choisi dans cette PHT par le registre d'historique global. La PHT à utiliser est choisie en fonction d'informations qui dépendent du branchement, typiquement les bits de poids faible de l'adresse du branchement. L'intérêt est de faire des prédictions faites sur mesure pour chaque branchement. Par exemple, le branchement X sera prédit comme pris, alors que le branchement Y sera non-pris, pour un historique global identique. C'est conceptuellement la méthode utilisée sur les unités gselect, mais avec une implémentation différente. L'''aliasing'' est aussi fortement réduit, bien que cette réduction soit semblable à celle obtenue avec les méthodes précédentes. Chaque PHT associée à un branchement est appelée une '''PHT locale''', à l'opposé d'une PHT unique appelée ''PHT globale''. [[File:Unité de prédiction de branchement avec des PHT locales et un historique global.jpg|centre|vignette|upright=2|Unité de prédiction de branchement avec des PHT locales et un historique global]] Au lieu d'un historique global, pour tous les branchements, il est possible d'utiliser un historique pour chaque branchement. Précisément, pour chaque entrée dans le BTB, le BTB mémorise le résultat des n dernières exécutions du branchement associé. Par exemple, si le registre d'historique contient 010, cela veut dire que le branchement a été non pris, puis pris, puis non pris. Un tel historique est appelé un '''historique local'''. Combiner PHT et historiques locaux donne une '''unité de prédiction à deux niveaux locale'''. Elle utilise des registres d'historique locaux de n bits, chacun étant couplé avec sa propre PHT locale contenant 2^n compteurs à saturation. Quand un branchement est exécuté, la PHT adéquate est sélectionnée, le registre d'historique de ce branchement est sélectionné, et l'historique local indique quel compteur à saturation choisir dans la PHT locale. [[File:Unité de prédiction à deux niveaux purement locale.png|centre|vignette|upright=2.5|Unité de prédiction à deux niveaux avec des historiques et des PHT locales.]] Implémenter cette technique demande d'ajouter un circuit qui sélectionne l'historique et la PHT adéquate en fonction du branchement. Le choix de la PHT et de l'historique se fait idéalement à partir de l'adresse du branchement. Mais c'est là une implémentation idéale, qui demande beaucoup de circuits pour un pouvoir de discrimination extrême. Généralement, l'unité de prédiction n'utilise que les bits de poids faible de l'adresse du branchement, ce qui permet de faire un choix correct, mais avec une possibilité d'''aliasing''. Pour récupérer l'historique local voulu, les bits de poids faible adressent une mémoire RAM, dont chaque byte contient l'historique local associé. La RAM en question est appelée la '''''Branch History Table''''', ou encore la '''table des historiques locaux''', que nous noterons BHT dans ce qui suivra. L'historique local est ensuite envoyé à la PHT adéquate, choisie en fonction des mêmes bits de poids faible du PC. Un bon moyen pour cela est d'accéder à toutes les PHT en parallèle, mais de sélectionner la bonne avec un multiplexeur, ce dernier étant commandé par les bits de poids faible du PC. [[File:Table des historiques locaux.jpg|centre|vignette|upright=2|Table des historiques locaux]] Cette unité de prédiction peut correctement prédire des branchements mal prédits par des compteurs à saturation. Tel est le cas des branchements dont le résultat est le suivant : pris, non pris, pris, non pris, et ainsi de suite. Même chose pour un branchement qui ferait : pris, non pris, non pris, non pris, pris, non pris, non pris, non pris, etc. En clair, il s'agit de situations où l'historique d'un branchement montre un ''pattern'', un motif qui se répète dans le temps. Notons que l'apparition de tels motifs correspond le plus souvent à la présence de boucles dans le programme. Quand une boucle s’exécute, les branchements qui sont à l'intérieur tendent à exprimer de tels motifs. Avec de tels motifs, les compteurs à saturation feront des prédictions incorrectes, là où les prédicteurs avec registre d'historique feront des prédictions parfaites. Par contre, elles sont assez mauvaises pour les autres types de branchements. Les boucles étant très fréquentes dans de nombreux programmes, de telles unités de prédiction donnent généralement de bons résultats. Un défaut des unités de prédiction purement locales de ce type est leur cout en termes de circuits. Utiliser un registre d'historique et une PHT par branchement a un cout en transistor élevé, avec tous les défauts que ca implique en termes de consommation électrique et thermique. Elles ont aussi des temps de calcul un peu plus important que les unités purement globales, mais pas de manière flagrante. Les gains en performance avec un historique local s'observent surtout pour les branchements qui sont dans des boucles ou pour les branchements utilisés pour concevoir des boucles, alors que l'implémentation globale marche bien pour les if..else (surtout quand ils sont imbriqués). Les deux sont donc complémentaires. Dans les faits, elles sont rarement utilisées seules du fait de leurs défauts, au profit d'unités de prédiction hybrides, qui mélangent historiques et PHT locaux et globaux. ===Les unités de prédiction à deux niveaux mixtes (globale/locale)=== Nous venons de voir les unités de prédiction de branchement à deux niveaux, aussi bien les implémentations avec un historique global (pour tous les branchements) et des historiques locaux (un par branchement). L'implémentation globale utilise un seul registre d'historique pour tous les branchements, alors que l’implémentation locale connaît l'historique de chaque branchement. Il est admis que l'historique global donne de meilleure prédiction que l'historique local. Mais les deux informations, historique global et historique de chaque branchement, sont des informations complémentaires, ce qui fait qu'il existe des approches hybrides. Certaines unités de prédiction utilisent les deux pour faire leur prédiction. Par exemple, on peut citer la '''prédiction mixte''' (''alloyed predictor''). Avec celle-ci, la table des compteurs à saturation est adressée avec la concaténation : de bits de l'adresse du branchement, du registre d'historique global et du registre d'historique local adressé par l'adresse du branchement. [[File:Prédiction mixte.jpg|centre|vignette|upright=2|Prédiction mixte.]] Une version optimisée de ce circuit permet d’accéder à la PHT et à la BHT en parallèle. Avec elle les bits de poids faible de l'adresse du branchement sont concaténés avec l'historique global. Le résultat est ensuite envoyé à plusieurs PHT locales distinctes, en parallèle. Les différences PHT envoient leurs résultats à un multiplexeur, commandé par l'historique local, qui sélectionne le résultat adéquat. L'inconvénient de cette mise en œuvre est que le circuit est assez gros, notamment en raison de la présence de plusieurs PHT séparées. [[File:Alloyed predictor optimisé.jpg|centre|vignette|upright=2|''Alloyed predictor'' optimisé]] ===La prédiction de branchement avec des perceptrons=== Les méthodes précédentes utilisent de un ou plusieurs comparateurs à saturation par historique possible. Sachant qu'il y a 2^n valeurs possibles pour un historique de n bits, le nombre de compteurs à saturation augmente exponentiellement avec la taille de l'historique. Cela limite grandement la taille de l'historique, alors qu'on aurait besoin d'un historique assez grand pour faire des prédictions excellentes. Pour éviter cette explosion exponentielle, des alternatives basées sur des techniques d'apprentissage artificiel (''machine learning'') existent. Un exemple est celui des unités de prédiction de branchement des processeurs AMD actuels se basent sur des techniques dites de ''neural branch prediction'', basée sur des perceptrons. La quasi-totalité des unités de prédiction de ce type se basent sur des perceptrons, un algorithme d'apprentissage automatique très simple, que nous aborderons plus bas. En théorie, il est possible d'utiliser des techniques d'apprentissage automatiques plus élaborées, comme les techniques de ''back-propagation'', mais cela n'est pas possible en pratique. Rappelons qu'une unité de prédiction de branchement doit idéalement fournir sa prédiction en un cycle d'horloge, et qu'un temps de calcul de la prédiction de 2 à 3 cycles est déjà un problème. L'implémentation d'un perceptron est déjà problématique de ce point de vue, car elle demande d'utiliser beaucoup de circuits arithmétiques complexes (multiplication et addition). Autant dire que les autres techniques usuelles de ''machine learning'', encore plus gourmandes en calculs, ne sont pas praticables. ====Les perceptrons et leur usage en tant qu'unité de prédiction de branchements==== L'idée derrière l'utilisation d'un perceptron est que l'historique est certes utile pour prédire si un branchement sera pris ou non, mais certains bits de l'historique sont censés être plus importants que d'autres. En soi, ce genre de situation est réaliste. Il arrive que certains branchements soient pris uniquement si les deux branchements précédents ne le sont pas, ou que si l'avant-dernier branchement est lui aussi pris. Mais les autres résultats de branchement présents dans l'historique ne sont pas ou peu utiles. En conséquence, deux historiques semblables, qui ne sont différents que d'un ou deux bits, peuvent donner des prédictions presque identiques. Dans ce cas, on peut faire la prédiction en n'utilisant que les bits identiques entre ces historiques, ou du moins en leur donnant plus d'importance qu'aux autres. On exploite alors des corrélations avec certains bits de l'historique, plutôt qu'avec l'historique entier lui-même. Pour coder ces corrélations, on associe un coefficient à chaque bit de l'historique, qui indique à quel point ce bit est important pour déterminer le résultat final. Ce coefficient est proportionnel à la corrélation entre le résultat du branchement associé au bit de l'historique, et le branchement à prédire. Notons que ces coefficients sont des nombres entiers qui peuvent être positifs, nuls ou négatifs. Un coefficient positif signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances de l'être aussi (et inversement). Un coefficient nul signifie que les deux branchements sont indépendants et que le bit de l'historique associé n'est pas à prendre en compte. Pour un coefficient négatif, cela signifie que si le branchement associé a été pris, alors le branchement à prédire a de bonnes chances d'être non-pris (et inversement). Les coefficients en question sont généralement compris entre -1 et 1. Une fois qu'on connait ces coefficients de corrélations, on peut calculer la probabilité que le branchement à prédire soit pris, avec des calculs arithmétiques simples. Par contre, cela demande que l'interprétation de l'historique soit modifiée. L'historique est toujours codé avec des bits, avec un 1 pour un branchement pris et un 0 pour un branchement non-pris. Mais dans les calculs qui vont suivre, un branchement pris est codé par un 1, alors qu'un branchement non-pris est codé par un -1 ! Ce détail permet de simplifier grandement les calculs. Dans sa version la plus simple, le perceptron calcule un nombre Z qui est positif ou nul si le branchement est pris, négatif s'il ne l'est pas. Tous les coefficients <math>p_i</math> sont alors des nombres relatifs, pouvant être positifs, nuls ou négatifs. Ils sont généralement compris entre -1 et 1. La formule devient donc celle-ci : : <math>Z = w_0 + \sum_i (h_i \times w_i)</math> [[File:ArtificialNeuronModel francais.png|centre|vignette|upright=2|Perceptron.]] : Sur le plan mathématique, le perceptron effectue un produit scalaire entre deux vecteurs : l'historique et le vecteur des poids. Choisir les coefficients adéquats est le but de l'algorithme du perceptron. L'unité de prédiction part de poids par défaut, qui sont mis à jour à chaque bonne ou mauvaise prédiction. Toute la magie de cet algorithme tient dans la manière dont sont mis à jour les poids. L'idée est de prendre le vecteur des poids, puis d'ajouter ou soustraire l'historique suivant le résultat du branchement. Les poids évoluent ainsi à chaque branchement, améliorant leurs prédictions d'une exécution à l'autre. La règle de mise à jour des poids est donc la suivante : : <math>w_i \leftarrow w_i + t \times h_i</math>, avec t = 1 pour un branchement pris, -1 pour un branchement non-pris. ====L'implémentation matérielle des perceptrons et ses optimisations==== Les calculs réalisés par un perceptrons sont simples, juste des multiplications et des additions et cela se sent dans la conception du circuit. Le circuit est composé de plusieurs perceptrons, un par entrée du BTB, un par branchement pour simplifier. Pour cela, on utilise une mémoire SRAM qui mémorise les poids de chaque perceptron. La SRAM est adressée par les bits de poids faible du ''program counter'', de l'adresse du branchement. Une fois les poids récupérés, ils sont envoyés à un circuit de calcul de la prédiction, composé de circuits multiplieurs suivis par un additionneur multi-opérande. Le circuit de mise à jour des poids récupère la prédiction, les poids du perceptron, mais aussi le résultat du branchement. La mise à jour étant une simple addition, le circuit est composé d'additionneurs/soustracteurs, couplés à des registres pour mémoriser la prédiction et les poids du perceptron en attendant que le résultat du branchement soit disponible. Vu qu'il y a un délai de quelques cycles avant que le résultat du branchement soit disponible et que les prédictions doivent continuer pendant ce temps, les poids du perceptron et la prédiction sont stockés dans des FIFOs lues/écrites à chaque cycle. [[File:Unité de prédiction de branchement basée sur des perceptrons.png|centre|vignette|upright=2|Unité de prédiction de branchement basée sur des perceptrons]] Rien de bien compliqué sur le principe, mais un tel circuit pose un problème en pratique. Rappelons que le perceptron doit fournir un résultat en moins d'un cycle d'horloge, et cela tient compte du temps nécessaire pour sélectionner le perceptron à partir de l'adresse du branchement. C'est un temps très court, surtout pour un circuit qui implique des multiplications. Or, le temps de calcul d'une addition est déjà très proche d'un cycle d'horloge, alors que les multiplications prennent facilement deux à trois cycles d'horloge. Autant dire que si on ajoute le temps d'accès à une petite SRAM, pour récupérer le perceptron, c'est presque impossible. Mais quelques optimisations simples permettent de rendre le calcul plus rapide. Premièrement, la taille de la SRAM est réduite grâce à quelques économies assez simples. Notamment, le perceptron utilise des poids codés sur quelques bits, généralement un octet, parfois 7 ou 5 bits, guère plus. Les poids sont encodés en complément à 1 ou en complément à 2, là où les perceptrons normaux utilisent des nombres flottants. Rien que ces deux choix simplifient les circuits et les rendent plus rapides. Le désavantage d'utiliser peu de bits par poids est une perte mineure en termes de taux de prédiction, qui est plus que compensée par l'économie en portes logiques, qui permet d'augmenter la taille de la SRAM. D'autres optimisations portent sur le circuit de calcul. Par exemple, la multiplication <math>h_i \times w_i</math> est facultative. Vu que l'historique contient les valeurs et -1, il suffit d'additionner le poids associé si la valeur est 1 et de le soustraire s'il vaut -1. On peut même simplifier la soustraction et se limiter à une complémentation à un (une inversion des bits du poids). En faisant cela, les circuits multiplieurs disparaissent et le circuit de prédiction se résume à un simple additionneur multi-opérande couplé à quelques inverseurs commandables. Certains perceptrons prennent en compte à la fois l'historique global et l'historique local pour faire leur prédiction. Les deux historiques sont simplement concaténés et envoyés en entrée du perceptron. Cela marche assez bien car les perceptrons peuvent utiliser des historiques de grande taille sans problèmes. Par contre, il y a besoin d'ajouter une ''branch history table'' au circuit, ce qui demande des portes logiques en plus, mais aussi un temps de calcul supplémentaire (l'accès à cette table n'est pas gratuit). ====Les avantages et inconvénients des perceptrons pour la prédiction de branchements==== L'avantage des unités de prédiction à base de perceptrons est qu'elles utilisent un nombre de portes logiques qui est proportionnel à la taille de l'historique, et non pas exponentiel. Cela permet d'utiliser un historique de grande taille, et donc d'obtenir un taux de bonnes prédictions très élevé, sans avoir à exploser le budget en portes logiques. Leur désavantage est que leurs performances de prédiction sont moins bonnes à historique équivalent. C’est la contrepartie du fait d'utiliser beaucoup moins de portes logiques. Là où les unités de prédictions précédentes avaient des taux de prédictions très corrects mais devaient se débrouiller avec des historiques courts, les perceptrons font l'inverse. Un autre désavantage est que les calculs des perceptrons prennent du temps, et leur prédiction peut facilement prendre deux voire trois cycles d’horloge. Un autre défaut est que les perceptrons ont des taux de prédiction correctes élevées seulement quand certaines conditions mathématiques sont respectées par les historiques. La condition en question est que les historiques correspondants à un branchement pris et les historiques où ce même branchement est non-pris doivent être linéairement séparables. La notion de linéairement séparable est un peu difficile à comprendre. Pour l'expliquer, imaginez que les N bits de l'historique forme un espace à N-dimensions discrets. Chaque historique est un point dans cet espace N-dimensionnel et chaque bit sert de coordonnée pour se repérer dans cet espace. Une fonction est linéairement séparable si l'espace N-dimensionnel peut être coupé en deux par un hyperplan : d'un côté de ce plan se trouvent les historiques des branchements pris, de l'autre se trouvent les historiques des branchements non-pris. Cet hyperplan est défini par l'ensemble des points qui respecte l'équation suivante : : <math>w_0 + \sum_i (h_i \times w_i) = 0</math> Un exemple où cette condition est respectée est quand le résultat d'une prédiction se calcule avec un ET entre plusieurs branchements de l'historique. Si un branchement est pris quand plusieurs branchements de l’historique sont tous pris ou tous non-pris, la condition est respectée et le perceptron donnera des prédictions correctes. Un exemple où cette condition n'est pas respectée est une banale fonction XOR. Prenons l'exemple d'un historique global de 2 bits. Imaginons que le branchement à prédire soit pris : soit quand le premier branchement de l'historique est pris et le second non-pris, soit quand le premier est non-pris et le second pris. Concrètement, la prédiction exacte se calcule en faisant un XOR entre les deux bits de l'historique. Mais dans ce cas, la condition "linéairement séparable" n'est pas respectée et le perceptron n'aura pas des performances optimales. Heureusement, les branchements des programmes informatiques sont une majorité à respecter la condition de séparation linéaire, ce qui rend les perceptrons parfaitement adaptés à la prédiction de branchements. Les unités de prédictions de branchement neuronales utilisent idéalement un perceptron par branchement. Cela demande d'associer, l'adresse du branchement pour chaque perceptron, généralement en donnant un perceptron par entrée du BTB. Mais une telle organisation n'est pas possible en pratique, car elle demanderait d'ajouter un ''tag'' à chaque perceptron/entrée du BTB, ce qui demanderait beaucoup de circuits et de ressources matérielles. Dans les faits l'association entre un branchement et un perceptron se fait en utilisant les bits de poids faible de l'adresse de branchement. Ces bits de poids faible de l'adresse de branchement sélectionnent le perceptron adéquat, puis celui-ci utilise l'historique global pour faire sa prédiction. Mais faire cela entrainera l'apparition de phénomènes d'''aliasing'', comme pour les unités de prédiction à deux niveaux. Les techniques vues précédemment peuvent en théorie être adaptées sur les unités à perceptrons, dans une certaine mesure. Notons que les perceptrons sont des algorithmes ou des circuits qui permettent de classer des données d'entrée en deux types. Ici, la donnée d'entrée est l'historique global et la sortie du perceptron indique si le branchement prédit est pris ou non-pris. Ils ne sont donc pas adaptés à d'autres tâches de prédiction, comme pour le préchargement des lignes de cache ou la prédiction de l'adresse de destination d'un branchement, ou toute autre tâche d'exécution spéculative un tant soit peu complexe. Leur utilisation reste cantonnée à la prédiction de branchement proprement dit, guère plus. Par contre, les autres techniques vues plus haut peuvent être utilisées pour le préchargement des lignes de cache, ou d'autres mécanismes de prédiction plus complexes, que nous aborderons dans la suite du cours. ==La prédiction avec plusieurs unités de branchement distinctes== Il est possible de combiner plusieurs unités de prédiction de branchement différentes et de combiner leurs résultats en une seule prédiction. On peut par exemple combiner une unité de prédiction spécialisée dans les boucles, une unité de prédiction 2 bits basée sur des compteurs à saturation, une unité de prédiction statique et une unité ''gshare''. Tout le problème est de décider quelle unité a fait la bonne prédiction, comment combiner les résultats des différentes unités de prédiction. Soit on choisit un résultat qui parait plus fiable que les autres, soit on combine les résultats pour faire une sorte de moyenne des prédictions. La méthode la plus simple de choisir le résultat d'une unité de prédiction parmi toutes les autres. Pour implémenter le tout, il suffit d'un multiplexeur qui effectue le choix de la prédiction. Reste à commander le multiplexeur, ce qui demande un '''circuit méta-prédicteur''' qui sélectionne la bonne unité de prédiction. Il arrive que le circuit méta-prédicteur soit conçu avec les mêmes techniques que les unités de prédiction de branchement elle-mêmes. Dans le cas le plus simple, il s'agit d'un simple compteur à saturation, mis à jour suivant la concordance des prédictions avec le résultat effectif du branchement. Mais c'est loin d'être systématique. Une autre solution combine le résultat des différentes unités de prédiction avec une fonction mathématique adéquate. Avec un nombre impair d'unités de prédiction, une méthode assez efficace est de simplement prendre le résultat majoritaire. Si une majorité d'unités pense que le branchement est pris, alors on le considère comme pris, et comme non pris dans le cas contraire. Par contre, avec un nombre pair d'unités, cette méthode simple ne marche pas, il y a un risque d'égalité. L'unité de prédiction de branchement « '''''e-gskew''''' » utilise trois unités gshare et combine leurs résultats avec un vote à majorité. Les trois unités gshare utilisent des XOR légèrement différents, dans le sens où les bits de l'adresse du branchement choisi ne sont pas les mêmes dans les trois unités, sans compter que le résultat du XOR peut subir une modification qui dépend de l'unité de prédiction choisie. Autrement dit, la fonction de hachage qui associe une entrée à un branchement dépend de l'unité. [[File:Unité de prédiction « e-gskew ».png|centre|vignette|upright=2|Unité de prédiction « e-gskew ».]] Enfin, une dernière solution est que certaines unités de prédiction ont la priorité sur toutes les autres. Par exemple, prenons un processeur avec un ''return stack predictor'', une unité qui détecte les boucles et une unité de prédiction avec un historique. Si le ''return stack predictor'' détecte une instruction de retour de fonction, sa prédiction passera avant toutes les autres unités de prédiction. Et dans ce qui va suivre, c'est cette solution qui sera utilisé pour les unités de prédiction qu'on va étudier. ===La prédiction des branchements des boucles=== Les boucles sont à part, pour ce qui est de la prédiction de branchement. Rappelons qu'une boucle FOR/WHILE répète une série d'instruction N fois, le nombre N étant appelé le '''compteur de boucle'''. Ce qui fait que les branchements d'une boucle FOR sont pris n − 1 fois, la dernière exécution étant non prise. La conséquence est que les unités de prédiction usuelles sont assez mauvaises pour prédire les branchements des boucles. Par contre, la prédiction statique marche particulièrement bien comparé au reste. La prédiction statique prédit que les branchements des boucles sont toujours pris, ce qui donne des prédictions qui sont d'autant meilleures que la boucle est répétée un grand nombre de fois. Reste à gérer les branchements des boucles au mieux, en sachant cela. L'idée est d'utiliser plusieurs unités de prédiction séparées : une spécialisée dans les boucles et d'autres unités de prédiction. Uen unité de méta-prédiction se charge d'activer l'unité de prédiction des boucles dès qu'une boucle est détéctée. Les unités de prédiction modernes essayent de détecter les branchements des boucles afin de les mettre à part. En soi, ce n'est pas compliqué les branchements des boucles sont presque toujours des branchements descendants et réciproquement. De plus, ces branchements sont pris à chaque répétition de la boucle, et ne sont non-pris que quand on quitte la boucle. Certaines unités de prédiction de branchement sont capables de prédire spécifiquement les branchements de boucles FOR imbriquées, à savoir où une boucle FOR est dans une autre boucle FOR. Concrètement, cela permet de répéter la première boucle FOR plusieurs fois de suite, souvent un grand nombre de fois. Avec les boucles imbriquées, le compteur de boucle peut changer d'une répétition à l'autre, mais ce n'est que rarement le cas. Dans de nombreux cas, le compteur de boucle reste le même d'une répétition à l'autre. Ce qui fait qu'une fois la boucle exécutée une première fois, on peut prédire à la perfection les exécutions suivantes. Pour cela, il suffit de déterminer le compteur de boucle après la première exécution de la boucle, puis de le mémoriser dans un compteur. Lors des exécutions suivantes de la boucle, on compte le nombre de fois que le branchement de la boucle s'exécute : il est prédit comme pris tant que le compteur est différent du compteur de boucle, mais non-pris en cas d'égalité. ===Le prédicteur L-TAGE=== Il est possible d'utiliser plusieurs unités de prédiction, chacune utilisant des historique de taille différente. Pour donner un exemple simple, on pourrait avoir une unité avec un historique de 2 bits, une autre avec un historique de 3 bits, une autre de 4 bits, etc. Une méta-unité de branchement prédit alors quelle taille d'historique donne les meilleurs résultats. Il est fréquent que les tailles d'historiques utilisées suivent une formule mathématique précise, appelée suite géométrique, ce qui est un mot bien barbare pour dire que l'on passe d'un historique au suivant en multipliant sa taille par une constante bien précise. Par exemple, on pourrait avoir des historiques de taille 2, 4, 8, 16, 32. C'est d'ailleurs le principe derrière les '''unités de branchement TAGE (''TAgged GEometric length predictor'')'''. Elles utilisent plusieurs registres d'historiques, chacun avec sa propre PHT, dont la taille suit une suite géométrique. Avec cette méthode, les branchements qui ont un motif répétitif sont alors parfaitement détectés, peu importe sa taille. Par exemple, un branchement avec un historique dont le cycle est pris, non-pris, non-pris, sera parfaitement prédit avec un historique de 3 bits, 6 bits, ou de 3*n bits, mais pas forcément avec un historique de 4 ou 8 bits. De ce fait, utiliser plusieurs unités de branchement avec plusieurs tailles d'historique fonctionne plutôt bien. Le prédicteur TAGE a été raffiné avec le temps, sur plusieurs points, que je ne vais pas détailler dans ce qui suit. Précisons cependant que les PHT sont structurées en caches, afin d'éliminer les phénomènes d'''aliasing''. L'ajout d'une unité de prédiction pour les boucles, a donné le '''prédicteur L-TAGE''', qui a lui-même été amélioré par la suite. Les unités de branchement TAGE sont les plus utilisées au moment où j'écris ces lignes (2026), et la majorité des processeurs x86 commerciaux utilisent une variante de ce prédicteur, à quelques exceptions près. ===La prédiction de branchement à deux niveaux=== Les processeurs modernes utilisent une prédiction de branchement à deux niveaux. Une première unité de branchement fournit des résultats rapides, mais peu précis. Elle envoie ses adresses au cache d'instruction, pour charger les instructions prédites, et met à jour le ''program counter'' en conséquence. La seconde est plus lente et fournit des résultats après quelques cycles d'horloge. Ces résultats sont pris en compte lors de l'étage de décodage d'instruction, histoire d'annuler des branchements considérés à tord comme pris/non-pris. Il y a donc une unité de prédiction rapide, dont le résultat est corrigé par une seconde unité. La seconde unité a le dernier mot, elle corrige ou annule les prédictions de la première. Ce qui lui vaut le nom, en anglais, d'''overiding predictor''. La correction se fait simplement en corrigeant le ''program counter'' et en forçant le décodeur à émettre des NOPs pendant quelques cycles, le temps d'éliminer les instructions chargées à tord. [[File:Double BTB sur les CPU modernes.png|centre|vignette|upright=2|Double BTB sur les CPU modernes.]] Les deux unités de prédiction peuvent utiliser des BTB séparés, de taille différente, au moins deux. Vu que les BTB sont des caches, on les appelle des '''BTB de niveau L1 et L2'''. Par exemple, les processeurs Bulldozer d'AMD disposent de deux BTBs : un BTB L1 de 512 entrées, et un BTB L2 de 5120 entrées. Un autres exemple est celui de l'architecture Silvermont d'Intel. Elle utilisait deux unités de prédiction : un BTB de petite taille avec des compteurs à saturation, et un prédicteur ''gshare'' de grande taille. Il faut noter que la BTB pour les branchements indirects vient en plus de ces deux BTBs. Elle est souvent utilisée pour compléter la BTB de niveau L2. Idem pour le ''return stack predictor'', qui vient en plus des BTB L1 et L2. Quelques processeurs ont trois niveaux de BTB, avec un niveau L0, L1 et L2. Pour donner quelques exemples, les processeurs Zen d'AMD disposent de plusieurs BTBs, avec des niveaux L0, L1 et L2. Le tableai ci-dessous vous donne lat aille et la latence de chaque niveau de BTB. {|class="wikitable" |- ! rowspan="3" | Zen 2 | 16 || 0 |- | 512 || 1 |- | 7168 || 4 |- ! rowspan="3" | Zen 3 | 16 || 0 |- | 1024 || 1 |- | 6656 + 1536 (branchements indirects)|| 4 |} ==Les optimisations de la prédiction de branchements== La prédiction de branchement est complexe et toute économie est bonne à prendre. Pour améliorer les performances, ou simplement améliorer l'utilisation du BTB et des PHT, diverses techniques ont été inventées. Ces techniques marchent quel que soit l'unité de prédiction prise en compte. Elles peuvent s'appliquer aussi bien aux unités basées sur des perceptrons, que sur des unités à deux niveaux ou des unités de prédiction dynamique en général. Ces techniques sont assez variés : certaines profitent du fait que de nombreux branchements sont biaisés, d'autres tentent de réduire les interférences entre branchements sans utiliser l'historique global/local, etc. ===Le filtrage de branchements=== Une première optimisation permet d'économiser l'usage des différentes ressources matérielles, comme la BTB ou les PHT, en les réservant aux branchements non-biaisés. Idéalement, les branchements biaisés peuvent être prédits en utilisant des techniques très simples. D'où l'idée d'utiliser deux unités de prédiction spécialisées : une pour les branchements biaisés et une autre plus complexe. L'unité de prédiction pour les branchements biaisés est une unité de prédiction à 1 bit qui indique si le branchement est pris ou non, ce qui suffit largement pour de tels branchements. Cette technique s'appelle le '''filtrage de branchement''' et son nom est assez parlant. On filtre les branchements biaisés pour éviter qu'ils utilisent des PHT et d'autres ressources qui gagneraient à être utilisées pour des branchements plus difficiles à prédire. Appliquer cette idée demande de reconnaître les branchements fortement biaisés d'une manière ou d'une autre, ce qui est plus facile à dire qu'à faire. Une solution similaire enregistre quels branchements sont biaisés dans le BTB, et utilise cette information pour faire des prédictions. ===L'usage de fonctions de hashage pour indexer les diverses SRAM/tables=== Plus haut, nous avons vu que les unités de prédiction de branchement contiennent des structures qui sont adressées par l'adresse de branchement. Tel est le cas du ''Branch Target Buffer'', de certaines PHT globales ou locales, de la ''Branch History Table'' qui stocke les historiques locaux, ou encore de la SRAM des poids des unités à base de perceptrons. En pratique, ces structures sont adressées non pas par l'adresse de branchement complète, mais pas les bits de poids faible de cette adresse. Cela a pour conséquence l'apparition d'un ''aliasing'' lié au fait que ces structures vont confondre deux branchements pour lesquels les bits de poids faible de l'adresse sont identiques. Il est possible d'utiliser autre chose que les bits de poids faible de l'adresse du branchement, afin de limiter les interférences. Et les possibilités sont multiples. Les possibilités en question s'inspirent des traitements effectués sur les adresses des banques dans les mémoires évoluées. Une première solution serait de faire un XOR entre les bits de poids faible de l'adresse du branchement, et d'autres bits de cette même adresse. Ainsi, deux branchements éloignés en mémoire donneraient des résultats différents, même si leurs bits de poids faible sont identiques. Une autre possibilité serait de diviser l'adresse du branchement par un nombre et de garder le reste. Ce calcul de modulo n'est en soi pas très différent du fait de conserver seulement les bits de poids faible. Conserver les N bits de poids faible consiste en effet à prendre le modulo par 2^N de l'adresse. Ici, l'idée serait de faire un modulo par un nombre P, qui serait un nombre premier proche de 2^N. Le fait que le nombre soit premier limite les cas où deux adresses différentes donneraient le même reste, ce qui réduit l'''aliasing''. Le fait de prendre un nombre proche de 2^N pour une entrée de N bits est que le résultat reste assez proche de ce qu'on obtiendrait en gardant les bits de poids faible, cette dernière étant une solution pas trop mauvaise. D'autres possibilités similaires se basent sur des réductions polynomiales ou d'autres astuces impliquant des nombres premiers. L'efficacité de ces méthodes dépend grandement de la taille de la SRAM/table considérée, des adresses des branchements et de beaucoup d'autres paramètres. La réduction des interférences par telle ou telle méthode dépend aussi de l'unité de prédiction considérée. Les résultats ne sont pas les mêmes selon que l'on parle d'une unité à perceptrons ou d'une unité à deux niveaux ou d'unités à base de compteurs à saturation, ou que l'on parle du BTB. Par contre, toutes les méthodes ne sont pas équivalentes en termes de temps de calcul ou de portes logiques. Autant la première solution avec un XOR ajoute un temps de calcul négligeable et quelques portes logiques, autant les autres méthodes requièrent l'ajout d'un diviseur très lent et gourmand en portes logiques pour calculer des modulos. Elles ne sont généralement pas praticables pour ces raisons, le temps de calcul serait trop élevé. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le pipeline | prevText=Le pipeline | next=Les optimisations du chargement des instructions | nextText=Les optimisations du chargement des instructions }} </noinclude> 380uxhas6or2fj5c31o3ezlicf1eupe Fonctionnement d'un ordinateur/Les processeurs superscalaires in-order 0 65956 773901 773837 2026-10-02T07:04:48Z DavidL 1746 773901 wikitext text/x-wiki Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''. Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire. En général, pour un processeur à émission multiple capable d'émettre N instructions en parallèle, on parle de '''processeur à N voies''', ou encore un ''processeur à N pipelines''. [[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]] Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur. Nous allons voir uniquement les processeurs superscalaires dans ce chapitre. Et plus précisément ceux sans exécution dans le désordre. Un processeur superscalaire peut utiliser l'exécution dans le désordre, mais ce n'est pas une obligation. Historiquement, les premiers processeurs superscalaires étaient des processeurs ''in-order'' (ce qui veut dire qu'ils n'utilisaient pas d'exécution dans le désordre). Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires ''in-order''. Dans ce chapitre, nous allons voir uniquement les processeurs superscalaires ''in-order'' pour simplifier les explications. Il faut cependant garder à l'esprit que les explications qui vont suivre marchent aussi pour les processeurs superscalaires ''out of order'', sauf pour quelques exceptions. Aussi, ce chapitre servira d'introduction au chapitre suivant. ==Le nombre de voies d'un processeur superscalaire== En général, les processeurs superscalaires ''in-order'' ne permettent que d'émettre deux micro-opérations à la fois. D'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs triple émission permettent eux d’émettre trois instructions à la fois, mais ils sont plus rares. A l'opposé, les CPU superscalaires les plus puissants peuvent émettre jusqu'à 8-10 instructions simultanément. En 2026, les CPU superscalaires à exécution dans le désordre peuvent monter jusqu’à 10 µops émises en même temps, ce qui est énorme ! Et cela nous amène à faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. Dans ce wikilivre, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4-5 µops à la fois, alors que les larges peuvent en émettre plus, l'intervalle entre les deux est une zone grise. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Nous parlerons de la '''largeur d'un CPU superscalaire''' pour parler du nombre de voies, du nombre de µops émises en même temps. Plus un CPU superscalaire est large, plus il est performant ''toutes choses égales par ailleurs''. Mais dans les faits, vous pouvez oublier la partie en italique. Élargir un CPU superscalaire demande de rajouter beaucoup de circuits. Et ce cout en circuit aura un impact sur la performance. À budget en transistors limité, il y a un compromis entre élargir un CPU et rajouter du cache ou la taille des ''schedulers''/ROB et autres. ===Les contraintes d’appariement=== Prenons un processeur double émission, capable d'émettre deux instructions simultanément. Émettre deux instructions en même temps n'est pas toujours possible. Par exemple, si deux instructions sont dépendantes, on ne peut pas les émettre en même temps. Mais oublions un instant ces histoires de dépendances de données, et partons du principe que le CPU ait affaire à deux instructions indépendantes, une paire idéale. Un CPU double émission "parfait" pourrait exécuter n'importe quelle paire d'instruction. Par exemple, il pourrait émettre deux multiplications consécutives, deux lectures consécutives, une lecture et une écriture en même temps, etc. En pratique, les processeurs qui font ça sont rare. Implémenter un CPU double émission "parfait" a un cout en circuit conséquent. Par exemple, pour émettre deux multiplications en même temps, il faudrait deux multiplieurs séparés. Idem pour exécuter deux accès mémoire simultanés : cela demande d'avoir deux unités mémoire et un cache multiport. La totalité des CPU double émission se débrouille avec un seul circuit multiplieur et une seule unité mémoire. Et cela entraine l'apparition de dépendances structurelles, qui font que certaines paires d'instructions sont impossibles. Par exemple, impossible d'émettre deux multiplications en même temps avec un seul circuit multiplieur. Mais émettre une multiplication en parallèle d'une addition est parfaitement possible. Il y a donc des paires d'instructions autorisées et des paires interdites. Pour le dire autrement, il y a des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Ces contraintes d’appariement ont un impact sur les performances, mais celui-ci peut être grandement mitigé. Les programmes informatiques utilisent en pratique beaucoup plus d'opérations entières de base que de multiplication, branchements et accès mémoire. Il y a environ 5 opérations entières de base pour une multiplication entière. En pratique, il est rare de devoir exécuter deux multiplications à la fois. Idem pour les décalages et rotations, qui sont assez éloignés les uns des autres. Deux branchements consécutifs sont quant à eux plus fréquents. Deux accès mémoire consécutifs est assez rare, mais ils restent assez proches les uns des autres malgré tout. La conséquence est que certaines paires d'instructions sont privilégiées par rapport au reste. Les paires regroupant deux calculs basiques (additions, soustractions, ...) sont systématiquement supportées, car dupliquer des ALU entières est facile. Les paires regroupant un calcul et une autre instruction sont elles aussi systématiquement supportées. Par contre, les paires avec deux branchements, deux accès mémoire, deux décalages/rotations, ou deux multiplications/divisions sont fréquemment interdites. Seuls les CPU superscalaires larges peuvent les permettre, et encore : sous conditions. Le résultat est que le processeur peut se débrouiller avec une seule unité mémoire et une unité de branchement. Décrire les contraintes d’appariement demande de dire quelles combinaisons d’instructions sont autorisées ou interdites. Et pour cela, nous allons introduire les abréviations suivantes : * INT est un raccourci pour parler d'une opération entière simple, qu'une ALU entière peut exécuter. * MUL correspond à une opération entière complexe, typiquement une multiplication, éventuellement à une division. * FLOAT désigne une opération flottante, peut importe laquelle. * MEM désigne un accès mémoire, à savoir une instruction ou µop mémoire. * BRANCH désigne une instruction de branchement. Cette terminologie permettra de définir des paires d'instructions, qu'elles soient possibles ou interdites. Par exemple, l'expression INT + FLOAT désigne une paire d'instruction regroupant une instruction entière et une instruction flottante, peu importe l'ordre des deux instructions. Il en sera de même pour les triplets, quadruplets, ou plus. Par exemple, l'expression INT + INT + FLOAT + MUL désigne un quadruplet d'instructions regroupant : deux instructions entières, une instruction flottante, et une multiplication entière. J'utiliserais aussi des parenthèses pour regrouper certaines combinaisons. Par exemple, l'expression INT + (MUL /FLOAT) veut parler d'une paire d'instruction où la première est une opération entière basique, la seconde est : soit une multiplication, soit une opération flottante. ===L'historique simplifié de la superscalairité=== L'histoire des CPU superscalaires a commencé dès 1960, mais est resté un projet de recherche pendant plusieurs décennies. De nombreux designs ont été conçus, mais abandonnés avant la production, comme l'ACS-1 d'IBM. Les années 1980 ont vu l'arrivée de prototypes de processeurs VLIW, mais qui n'étaient pas superscalaires. Par exemple, l'Apollo DN10000 et l'Intel i860 pouvaient émettre deux instructions à la fois, sous conditions, mais demandaient que le compilateur fasse une bonne partie du travail. Les premiers CPU superscalaires commercialisés sont sortis en 1989. Deux CPUs superscalaires sont sortis cette année-là : le RS/6000 d'IBM et l'i960CA d'Intel. Puis, la compétition s'est aussi lancée dans la bataille. Les CPU commercialisés avant l'année 1993 étaient à double émission, à l'exception du SuperSPARC qui utilisait la triple émission. Les modèles après 1993 hésitaient entre double et quadruple émission. Dans le tableau ci-dessous, les processeurs quadruple émission sont en bleu, les triple émission sont en rouge, les autres ne sont pas en couleur. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | RS/6000 || || || i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | RSC || PA 7100 || class="f_rouge | SuperSPARC || || 21064 || |- ! 1993 | PPC 601 || || HyperSPARC || Pentium || || |- ! 1994 | class="f_bleu | POWER 2 || PA 7200 || || || class="f_bleu | 21164 || |- ! 1995 | 603/604 || class="f_bleu | PA 8000 || class="f_bleu | UltraSPARC 1 || Microarchitecture P6 (+ concurrence AMD) || || class="f_bleu | MIPS R1000 |- ! 1996 | class="f_bleu | 620 || || || || class="f_bleu | 21264 || |- ! ... || ... || ... || ... || ... || ... || ... |} ==Le ''front-end'' des processeurs superscalaires== Un processeur superscalaire doit charger, décoder, et exécuter plusieurs instructions par cycle. Par exemple, un processeur triple émission doit charger 3 instructions lors d'un premier cycle, puis les décoder au cycle suivant, et les exécuter au cycle suivant. Il travaille donc pas paquets de 3 instructions, dans cet exemple. Si c'est un processeur superscalaire ''in-order'', cela donne l'impression que le pipeline dispose de plusieurs '''pipelines séparés'''. Intuitivement, charger, décoder et exécuter plusieurs instructions à la fois demande de dupliquer des circuits. Par exemple, il faut dupliquer les décodeurs pour décoder plusieurs instructions. Les unités de calcul sont aussi dupliquées, sauf sur certains processeurs qui arrivent à ruser, mais laissons cela de côté pour plus tard. Mais d'autres circuits sont simplement modifiés, comme l'unité de chargement ou l'unité d'émission. Voyons ce qu'il en est dans le détail. {|class="wikitable" |- ! rowspan="2" | Processeur sans émission multiple | rowspan="2" | Chargement | rowspan="2" | Décodage | rowspan="2" | Émission | Exécution / ALU | rowspan="2" | ''Commit''/ROB |- | Exécution / ALU |- ! rowspan="4" | Processeur superscalaire | rowspan="4" | Chargement | rowspan="2" | Décodage | rowspan="4" | Émission | Exécution / ALU | rowspan="4" | ''Commit''/ROB |- | Exécution / ALU |- | rowspan="2" | Décodage | Exécution / ALU |- | Exécution / ALU |} Nous allons d'abord voir ce qu'il en est pour le ''front-end'', à savoir la partie du CPU qui s'occupe de charger et décoder les instructions. Elle regroupe l'unité de chargement, les décodeurs, l'unité de prédiction de branchement, la file de µops, la file d'instruction, et divers caches. En soi, les changements portent surtout sur l'unité de chargement et les décodeurs. ===Le chargement de plusieurs instructions consécutives=== Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large. Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Charger plusieurs instructions consécutives ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et il faut gérer leur présence. * Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. * Avec prédiction de branchement, les instructions situées après le branchement ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. [[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]] Un autre défaut est que sur le CPU charge un bloc de 128 bits à la fois, les instructions doivent être alignées sur 128 bits. Et on peut généraliser à n'importe quelle taille de bloc. Si les instructions ne sont pas alignées, il faut gérer la situation. Il arrive qu'une instruction soit à cheval sur deux blocs, avec un morceau dans le premier bloc, un autre dans le second. Dans ce cas, l'instruction est chargée en deux fois, et des circuits doivent gérer la situation pour recoller les morceaux. Sur les CPU avec des instructions de taille fixe, le problème ne survient que rarement. Mais il est fréquent avec des instructions de taille variable. Un autre défaut est lié aux branchements. Imaginez qu'un branchement s'exécute et branche vers une instruction de destination. Rien n'indique que l'instruction de destination sera au tout début d'un bloc. Si ça se trouve, elle sera en troisième position d'un bloc de 4 instructions. Une solution simple charge le bloc en respectant l'alignement, et les deux premières instructions du bloc ne sont pas à exécuter. Il y a donc une petite perte de performance. Certains CPU, comme l'IBM RS/6000, ont la machinerie charger un bloc de N instructions même s'il n'est pas aligné. La machinerie en question se trouve dans le cache d'instruction et est tout sauf simple à expliquer, ce qui fait que je me permets de la passer sous silence. Un détail, cependant : elle ne permet pas de charger des blocs qui sont à cheval sur deux lignes de cache. ===Le décodage parallèle des instructions=== Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies : le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits. Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur. Il faut noter qu'une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante. Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Un exemple classique était les processeurs Athlon d'AMD, qui pouvaient décoder deux instructions par cycle, mais émettre 4 µops par cycle. Ce n'était pas un problème, car beaucoup d'instructions, même basiques, étaient décodées en 2 µops. ===L'émission parallèle des instructions=== La superscalarité modifie en profondeur l'unité d'émission, notamment au niveau de son interface. Avant, elle recevait une micro-opération sur son entrée, et fournissait la micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', un ''scheduler'' ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Dans ce qui suit, nous ne parlerons pas d'entrée et de sortie, mais de '''ports de décodage''' et de '''ports d'émission'''. Les ports de décodage reçoivent des micro-opérations provenant de l'unité de décodage, les ports d'émissions sont pour les micro-opérations émises. Les ports d'émission injectent les micro-opérations émises dans le pipeline, elles sont propagées au banc de registre, aux ALUs, etc. Un processeur superscalaire a forcément plusieurs ports d'émission (et de décodage). [[File:Ports de décodage et d'émission.png|centre|vignette|upright=2|Ports de décodage et d'émission]] Un détail important est que l'unité d'émission peut avoir plus de ports d'émission que de ports de décodage. Et n'allez pas croire qu'un surplus de ports d'émission est du gâchis. Sur les processeurs avec exécution dans le désordre, ce surplus peut être utilisé pour émettre une µops sur chaque port d'émission par cycle d'horloge, ce qui donne un gain de performance appréciable. Ceci dit, les processeurs sans exécution dans le désordre utilisent aussi des ports d'émission en surplus. La raison sera expliquée dans quelques sections, mais nous pouvons dire que cela simplifie la conception du processeur. Si l'unité d'émission a un surplus de ports d'émission, on doit alors distinguer sa ''largeur de décodage'' et sa ''largeur d'émission''. La '''largeur de décodage''' est le nombre de µops fournies par l'unité de décodage par cycle d'horloge, la '''largeur d'émission''' est le nombre maximal de µops émises par cycle d'horloge. La largeur d'émission est égale ou supérieure à la largeur de décodage, tout dépend du processeur. [[File:Processeur superscalaire avec un excès de ports d'émission.png|centre|vignette|upright=2|Processeur superscalaire avec un excès de ports d'émission]] Si l'interface de l'unité d'émission change, l'intérieur est tout autant modifié. Sur un CPU non-superscalaire, l'unité d'émission reçoit une µop "candidate à l'émission", et détecte les dépendances entre avec les µops en vol, en cours d'exécution dans le pipeline. Et c'est toujours le cas sur les processeurs superscalaires : l'unité d'émission détecte les dépendances entre les µops candidates et les µops en vol. Mais avec la siperscalarité, elle doit en plus tester les dépendances entre les µops candidates. Si l'unité d'émission est un ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément et les sérialiser si besoin. Avec un ''scheduler'', il n'y a pour ainsi dire rien à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil gèrent les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires modernes n'utilisent pas de ''scoreboard''. ==Le ''back-end'' des processeurs superscalaires== Posons-nous cette question : de combien d'unités un processeur superscalaire a besoin ? Les unités en question ne sont pas nécessairement des unités de calcul, l'unité mémoire et l'unité de branchement comptent aussi. La réponse dépend du nombre de voies. Si le ''front-end'' charge et décode un paquet de N instructions, exécuter ces N instructions simultanément demandera là aussi au moins N unités de calcul. Notez le terme : ''au moins'', car c'est là tout l'enjeu de cette section. La réponse dépend aussi des contraintes d'appariement. Pour un CPU superscalaire "parfait", sans contraintes d’appariement, toutes les unités doivent être dupliquées, y compris l'unité mémoire. Un processeur à double émission parfaite devrait dupliquer l'ALU entière, le ''barrel shifter'', le circuit multiplieur, la FPU, l'unité mémoire, et l'unité de branchement, ce qui ferait passer de 6 unités à 12. Le cout en circuit est énorme, même pour de la double émission ! Et pour un CPU à N pipelines, le cout en circuit encore plus prohibitif ! En pratique, les processeurs superscalaires qui font ça sont très rares. La majeure partie des processeurs superscalaires utilisent d'autres techniques : le partitionnement, la duplication partielle des ALUs, la duplication des autres unités. ===Le partitionnement des unités fonctionnelles=== Les CPU superscalaires les plus simples ne dupliquent pas leurs unités de calcul, ils se contentent d'exploiter les unités existantes. Et pour expliquer comment, nous allons partir de l'exemple d'un processeur non-superscalaire contient une ALU entière, une FPU, une unité mémoire et une unité de branchement. Les quatre unités peuvent fonctionner en parallèle, à savoir qu'une unité peut exécuter une instruction à elle toute seule, sans interagir avec les autres. Cela suffit pour créer un processeur quadruple émission, qui peut émettre en même temps : une opération entière, une opération flottante, un branchement, un accès mémoire. Le principe marche aussi sur un processeur avec moins de 4 unités, par exemple sans FPU et/ou sans unité de branchement, ou avec au contraire des unités en plus. L'idée générale est d’émettre une µops séparée dans chaque unité fonctionnelle. Utiliser ainsi les unités existantes, pour rendre un CPU superscalaire, s'appelle le '''partitionnement superscalaire'''. Il consiste à attribuer un port d'émission à chaque unité existante. Le terme n'est pas beaucoup utilisé en dehors de quelques vieux articles académiques. Le partitionnement est fortement dépendant du jeu d'instruction, car il demande de bien séparer instructions entières, flottantes, branchements et instructions mémoire. Et autant c'est bien le cas sur les CPU RISC, autant les CPU CISC ne sont pas dans ce cas. Les CPU CISC ont des instructions ''load-op'', qui effectuent une instruction arithmétique et une µop mémoire. Et ces instructions sont très compliquées à implémenter avec le partitionnement. Et au-delà de ça, les CPU CISC ont beaucoup d'instructions qui sont décodées en plusieurs µops, à exécuter dans un ordre bien précis. On peut pas émettre les µops en question simultanément. Le partitionnement a un cout en circuit très limité, pour un gain en performance lui aussi limité. Pour reprendre le CPU de l'exemple précédent, il est rare que l'on ait à exécuter ces quatre instructions en même temps. En pratique, les combinaisons fréquentes sont les combinaisons calcul + accès mémoire, calcul + branchement, calcul entier + calcul flottant. Utiliser une quadruple émission de ce type n'est donc pas une bonne idée, alors que la double émission parait plus adaptée. Avant de poursuivre, précisons que la FPU regroupe un additionneur flottant et un multiplieur flottant. Et ce fait peut être exploité pour approfondir le partitionnement. La '''double émission FADD/FMUL''' permet d'émettre une addition flottante en même temps qu'une multiplication flottante. Elle a été utilisée sur les processeurs x86, Alpha et SPARC. Les autres circuits de calcul flottant sont souvent regroupés avec l'additionneur. La division est parfois e même port que le multiplieur flottant, parfois sur celui de l'additionneur (quand elle est microcodée et exécutée en enchainant les soustractions). ===La duplication des ALU entières=== Le partitionnement est une technique élégante, simple, facile à implémenter et au cout en transistors modeste. Mais il faut avouer que cela ne donne pas des CPU superscalaires très efficaces. Le partitionnement permet d'émettre des paires, triplets, quadruplets de micro-opérations qui sont rares en pratique. Et les paires, triplets, quadruplets d'instructions courants ne sont pas supportés. Les contraintes d’appariement sont trop strictes. Les combinaisons les plus fréquentes en pratique sont des paires/triplets d'opérations entières, souvent des paires/triplets d'additions ou d'opérations entières simples. Il est donc primordial d'émettre plusieurs opérations entières simultanément. Le fait d'émettre et d'exécuter plusieurs opérations entières en même temps s'appelle l''''émission entière multiple'''. Pour exécuter la seconde/troisième opération entière, il faut dupliquer les ALU entières (ou réutiliser l'AGU comme ALU entière comme le fait l'Intel i960). Le processeur doit avoir deux ALU entières pour de la double émission entière, trois pour de triple émission entière, etc. De plus, il faut ajouter des ports au banc de registre, pour lire deux fois plus d'opérandes et décrire deux résultats. Et enfin, il faut rajouter un port d'émission pour pouvoir émettre une micro-opération entière en plus. [[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]] La tendance est de multiplier les ALU entières, mais pas le circuit multiplieur. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Le CPU peut émettre deux additions en même temps, ou une addition et une multiplication, mais pas deux multiplications. Il en est de même avec le ''barrel shifter'', qui n'est pas dupliqué. Il est donc possible d'émettre un décalage avec une opération de base, mais pas deux décalages en parallèle. Mais ces contraintes d’appariement ne sont pas un problème, car il est rare de devoir émettre deux multiplications/décalages en même temps. La plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. La moyenne est proche d'une multiplication pour 4/5 additions. Vu que des multiplications consécutives sont très rares, dupliquer le multiplieur est presque inutile. Disposer de plusieurs circuits multiplieurs ou de plusieurs ''barrel shifters'' serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle. Il faut noter que sur les CPU 32 bits, les multiplications peuvent être exécutées dans la FPU ou dans un multiplieur séparé, tout dépend de si le CPU exécute les multiplications dans la FPU ou pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | ALU entière || ALU entière || FPU |} Reste à connecter les unités de calcul à l'unité d'émission (indirectement, car il y a des étages de pipeline entre les deux). Une implémentation basique utilisée un port d'émission par unité. Prenons l'exemple d'un CPU superscalaire avec 2 ALU entières, un multiplieur entier, un ''barrel shifter'' une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. C'est beaucoup, l'unité d'émission est très complexe. Suffisamment complexe pour se demander s'ils n'existerait pas des optimisations qui permettraient d'en réduire le nombre sans trop réduire les performances, ni le nombre d'ALU. Et cela nous amène à la section suivante. ===Le partage des ports d'émission=== La présence d'un circuit multiplieur et d'un ''barrel shifter'' pose des problèmes pour le partitionnement. Il est possible d'avoir un port d'émission dédié au multiplieur, et un autre au ''barrel shifter'', qui sont séparés de ceux pour l'ALU entière. Il est alors possible d'émettre une opération entière, une multiplication et un décalage, simultanément. Nous parlerons de '''partitionnement entier''' pour désigner cette possibilité. [[File:Partitionnement superscalaire entier.png|centre|vignette|upright=2|Partitionnement superscalaire entier.]] Le partitionnement entier est cependant très rare, car le gain en performance serait trop faible au regard des couts en circuit. Quelques processeurs historiques l'utilisaient, mais ce n'est plus le cas de nos jours. La raison est que les multiplications et les décalages sont assez rares dans le code, ce qui fait que le port d'émission associés sont sous-utilisés. Et un port d'émission a un cout en circuit assez important. Et c'est sans compter qu'ajouter des ports sur le banc de registres juste pour un multiplieur sous-utilisé n'en vaut pas la peine. L'idéal serait de trouver un moyen de réduire le nombre de ports d'émissions, sans réduire le nombre d'unités de calcul. [[File:Emission multiple des opérations entières, implémentation naive.png|vignette|Émission multiple des opérations entières, implémentation naive]] La solution consiste à '''partager les ports d'émission''', à savoir connecter un port d'émission à plusieurs unités de calcul. L'exemple classique est justement lié aux circuits multiplieur et ''barrel shifters''. L'idée est que le multiplieur et le ''barrel shifter'' sont mis sur le même port d'émission qu'une ALU entière. Prenons par exemple le processeur à double émission entière illustré ci-contre. Un port d'émission est relié à une ALU entière seule, et l'autre port d'émission alimente la seconde ALU entière et le multiplieur. La conséquence est que le processeur peut émettre deux opérations entières, dont une multiplication. Si on avait utilisé un port d'émission dédié au multiplieur, il aurait été possible d'émettre deux opérations entières et une multiplication. On perd donc une voie, mais on gagne en flexibilité et on économise des circuits. Notons que ce partage signifie aussi que le multiplieur partage les ports sur le banc de registre, ce qui fait une double économie. [[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, implémentation courante]] En général, le ''barrel shifter'' et le multiplieur sont placés sur deux voies d'émission séparés, ce qui permet d'émettre un décalage et une multiplication en même temps. Les mettre sur le même port ne le permettrait pas. {|class="wikitable" |- ! Voie d'émission n°1 !! Voie d'émission n°2 !! Voie d'émission n°3 |- | * ALU entière * Multiplieur | * ALU entière * Barrel shifter | FPU |- | * Additions, soustractions, opérations bit à bit, etc. * Multiplication | * Additions, soustractions, opérations bit à bit, etc. * Décalages et rotations | Opération flottante |} Terminons concernant les multiplieurs avec une dernière subtilité. Pour rappel, les processeurs modernes intègrent en réalité deux multiplieurs différents. Le résultat d'une multiplication est très large et prend deux registres. Par exemple, une multiplication de deux opérandes 64 bits donne un résultat sur 128 bits, ce qui ne tient pas dans un registre, mais en prend deux. L'instruction de multiplication classique ne calcule que les 64 bits de poids faible. mais elle est complétée par d'autres instructions de multiplication qui peuvent faire le calcul complet. Pour résoudre le problème, les processeurs modernes intègrent deux instructions de multiplications : une qui calcule les bits de poids faible du résultat, une autre qui calcule les bits de poids fort. Et ces deux instructions sont associées à deux multiplieurs séparés, aux circuits différents. Le multiplieur qui calcule les bits de poids faible du résultat est appelé le ''low multiplier'', l'autre est appelé le ''high multiplier''. Le second multiplieur est souvent placé sur un second port d'émission, différent du premier multiplieur, partagé avec une ALU. L’avantage de mettre les deux multiplieurs sur deux ports d'émission différents est qu'une multiplication complète peut être réalisée en utilisant les deux multiplieurs en parallèle. Les deux sous-multiplications sont émises en même temps, exécutées en même temps. pas besoin d'exécuter l'une puis la seconde, d'où un gain performance appréciable. Nous venons de voir comment partager un port d'émission entre une ALU et un multiplieur. Mais au-delà de cet exemple, le partage des ports d'émission peut appliquer à n'importe quelle unité. Prenons l'exemple d'un CPU superscalaire avec 4 ALU entières, une FPU, et une unité mémoire. Vu qu'il y a 6 unités, cela fait donc 6 ports d'émission. Cependant, il est rare qu'un bloc de 6 instructions contienne exactement 4 opérations entières, une opération flottante, et un accès mémoire. Beaucoup de ports d'émission seraient sous-utilisés. La solution est de partager des ports d'émission, pour passer à la quadruple émission. Le CPU peut alors répartir 4 instructions sur les ports d'émission adéquats, à chaque cycle. Le CPU a donc 4 ports de décodage et 6 ports d'émission. Une implémentation classique permet n'importe quelle combinaison de 4 instructions compatibles avec les 6 unités. Par exemple, il devient possible d'émettre simultanément 4 additions, la combinaison 2 * INT + FLOAT + MEM, 3 * INT + MEM, et bien d'autres. L'exemple relie une ALU entière, la FPU et l'unité mémoire sur le même port. Le CPU a alors quatre ports d'émission, qui ne sont pas "identiques". Trois d'entre eux sont reliés à une ALU entière chacun, rien d'autre. Le quatrième est lui relié à une ALU entière, la FPU et l'unité mémoire. On économise alors deux ports d'émission, mais cela entraine l'apparition de contraintes d’appariement. Le CPU peut émettre 3 opérations entières + n'importe quelle autre opération. Mais les autres combinaisons sont interdites. Le CPU peut émettre 6 opérations entières simultanément, grâce à 6 ALU entières. Les accès mémoire ou opérations flottantes ne viennent pas en plus, mais en remplacement des opérations entières. Regrouper plusieurs ALU sur un même port d'émission est donc à l'origine de dépendances structurelles, de nouvelles contraintes d’appariement. Dans certaines situations de '''''port contention''''', on a assez d'ALU pour exécuter N micro-opérations, mais la répartition des ALUs sur les ports l'empêche. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port d'émission. On peut réduire les cas de ''port contention'' en répartissant correctement les ALU sur les ports d'émission, mais sans jamais les supprimer complétement. Un exemple est celui du PowerPC 604. Il implémentait la quadruple émission, alors qu'il avait 5 unités de calcul. Mais deux unités partageaient un même port d'émission. Dans le détail, il avait deux ALU entières, un circuit multiplieur/diviseur, une FPU, une unité mémoire. La FPU et le multiplieur étaient sur le même port d'émission. En conséquence, le CPU pouvait émettre seulement les combinaisons INT + INT + MEM + (MUL/FLOAT). : Il est intéressant de voir l'exemple d'un processeur superscalaire parfait, sans contraintes d'appariement, à N voies. Pour cela, on part d'un processeur non-superscalaire, avec M unités de calcul. Un tel processeur peut s'implémenter avec N ports d'émission, chacun relié à M unités de calcul possibles, ce qui fait que les M unités de calcul sont dupliquées en N exemplaires. Si on prend par exemple un CPU non-superscalaire avec une ALU, une FPU, une unité mémoire et une unité de branchement, un port d'émission sera relié à une ALU, une FPU, une unité mémoire et une unité de branchement. Dupliquer les ALU entière est une première étape, mais il est aussi possible de dupliquer d'autres unités, comme l'unité mémoire ou l'unité de branchement. La rentabilité en performance dépend surtout de si cela autorise des paires d'instructions fréquentes. Le cout en circuit n'est aussi pas le même. Sur les CPU superscalaires ''in-order'', seuls les ALU et FPU sont dupliquées. ===Les CPU superscalaires mélangent les trois techniques=== Les CPU superscalaires utilisent toutes les techniques à la fois : partitionnement, duplication de l'ALU entière, duplication des autres unités fonctionnelles. Chaque processeur utilise ces trois stratégies à sa sauce, en fonction de son budget en transistor. Les quatre techniques ont cependant un cout en circuit différent, et donnent des gains de performances eux aussi forts différents. * Le '''partitionnement superscalaire''' a un cout en circuit modéré, car on n'a pas besoin de dupliquer d'ALU. * La '''duplication des ALU entières''' est la technique la plus importante : elle a un cout en circuit modéré, et donne des gains en performance impressionnants. * Le '''partage des ports d'émission''' réduit le cout en circuit du CPU, pour des pertes de performances mineures si bien fait. L'usage du partitionnement seul, sans duplication, était surtout le fait des processeurs commercialisés entre les années 1989 et 1993. De nos jours, les processeurs utilisent un mix différent d'unités, et leur partitionnement est différent. Ils tendent à s'adapter au mieux aux besoins des programmes exécutés. D'après diverses estimations, un programme informatique exécute environ 40% de son code dans des instructions arithmétiques, un autre 40% dans les accès mémoire, et un le reste pour... le reste. L'idéal est donc de pouvoir émettre autant de µops mémoire que de µops entières, à chaque cycle. Dit autrement, la largeur du processeur doit pouvoir être répartie ''moitié opération - moitié mémoire''. Bien évidemment, le processeur peut gérer d'autres combinaisons. Pour s'adapter, les processeurs dupliquent d'abord les ALU entières, suivi par l'unité mémoire, et enfin par les autres unités. Pour s’adapter au mieux, un processeur à N voies doit disposer d'au moins N/2 ALU entières, l'idéal étant d'aller au-delà. Les CPU superscalaires historiques avaient 2 ALU pour de la quadruple émission, ce qui était très limite. Les CPU superscalaires modernes mettent le paquet niveau ALU entière. La plupart des CPU x86 modernes descendent rarement sous les 2/3, la plupart remplissent presque la totalité de la largeur du CPU. Pour donner un exemple, la micro-architecture Golden Cove d'Intel est un processeur à 6 voies, avec 5 ALU entières. Ensuite, les processeurs modernes dupliquent leurs unités mémoire. Par exemple, la micro-architecture ''Golden Cove'' d'Intel dispose de trois unités mémoire : deux spécialisées dans les écritures, trois dans les lectures. La micro-architecture ''Gracemont'' utilise la quintuple émission et peut émettre 4 µops entières et 4 µops mémoire. Notez que le ratio est équilibré entre opérations entières et mémoire, avec soit égalité, soit un avantage pour les ALU entières. Une autre régularité est que la largeur totale du processeur peut être occupée par des opérations entières/mémoire. Pour le dire autrement, sur un processeur quadruple émission, il pourra émettre un mix de 4 instructions entière/mémoire, avec zéro opération flottante. Idem avec des processeurs triples ou quintuple émission, voire plus. Si le processeur émet une opération flottante, elle remplace une instruction entière ou mémoire. Prenons l'exemple des processeurs AMD K6, sortis en 1997, juste avant les AMD Athlon. Ils avaient 10 unités de calcul, 6 ports d'émission, 4 ports de décodage. Voici exactement comment étaient répartie les unités de calcul sur les ports d'émission. C'était des processeurs quadruple émission, ce qui fait que la moitié de la largeur du processeur sert pour des opérations entières, et l'autre moitié pour les accès mémoire. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || FPU || Unité de branchement || Unité LOAD || Unité STORE |- | || Multiplieur || || || || Unité de calcul d'adresse (instruction LEA) |- | || ''Barrel shifter'' || || || || Unité pour les instructions PUSH |} Les micro-architectures K7, K8 et K10, utilisées sur les anciens CPU AMD Athlon, sont un exemple moins représentatif. Ces micro-architectures utilisent le partage des ports d'émission au maximum. C'était des micro-architectures à triple émission, avec six ports d’émission. Il y avait trois ports d'émission pour les opérations flottantes, et trois ports d'émission pour les micro-opérations entières/mémoire. Il y avait trois ALU entières et trois unités mémoire, chaque ALU partageait un port d'émission avec une unité mémoire. Elle pouvait donc émettre soit trois opérations entières, soit trois µops mémoire, soit un mix des deux. En plus des mix avec les opérations flottantes et branchements. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 ! |- | ALU entière || ALU entière || ALU entière || FADD || FMUL || FPU (autres opérations) |- | Multiplieur || || || || || |- | LOAD/STORE || LOAD/STORE || LOAD/STORE || || || |} Les CPU superscalaires larges dupliquent les multiplieurs et/ou les ''barrel shifters'', et même les unités de branchement. La raison est que quand la largeur du CPU augmente, le processeur traite des paquets d'instructions de plus en plus gros. Par exemple, pour un CPU à 10 voies, cela veut dire émettre 10 instructions en même temps. Il est alors plus fréquent d'avoir deux multiplications, deux décalages, deux accès mémoire, deux branchements. Par exemple, la moyenne est d'une multiplication tous les 5 instructions. Les CPU superscalaires à 10 voies ont donc tendance à avoir deux multiplications par paquet d'instruction, en moyenne. Avoir deux multiplieurs est alors un gain en performance clair. Par exemple, la micro-architecture ''Golden Cove'' a deux ''barrel shifters''. C'est une architecture à 6 voies. {|class="wikitable" |- ! Port d'émission n°1 !! Port d'émission n°2 !! Port d'émission n°3 !! Port d'émission n°4 !! Port d'émission n°5 !! Port d'émission n°6 !! Port d'émission n°7 !! Port d'émission n°8 !! Port d'émission n°9 !! Port d'émission n°10 |- | ALU entière || ALU entière || ALU entière || ALU entière || ALU entière || LOAD || LOAD || LOAD || STORE || STORE |- | ''Barrel shifter'' || Multiplieur || Multiplieur || ''Barrel shifter'' || || || || || || |- | Branchements || Diviseur || || Branchements || || || || || || |} ==Les processeurs à double émission== Dans cette section, nous allons étudier la forme la plus basique de superscalarité : les processeurs à double émission. De tels processeurs n'utilisent généralement pas l'exécution dans le désordre, mais il y a quelques exceptions. Les premiers CPU à double émission sont assez représentatifs des CPU à double émission modernes. Il faut dire que se limiter à deux voies ne permet pas de faire beaucoup de choses. En général, les processeurs à double émission ne dupliquent pas l'unité mémoire. Cela ne servirait à rien de faire deux accès mémoire simultanés sur un processeur à double émission. Et il en est de même pour la FPU et l'unité de branchement. Ils se limitent à deux techniques : dupliquer l'ALU entière, le partitionnement. Quant au partage des ports d'émission, c'est là que les ennuis commencent. L'implémentation la plus simple de la double émission utilise deux ports d'émission. Elle donne l'illusion que le processeur dispose de '''deux pipelines séparés'''. Ils partent d'un processeur non-superscalaire, et ajoutent un second pipeline, en sortie de l'unité d'émission. Le second pipeline permet l'émission parallèle d'une seconde instruction. Le problème est que les deux ports d'émission sont reliés à un grand nombre d'unités de calcul. L'unité d'émission doit donc partager des ports d'émission, et c'est là que les subtilités commencent. Il faut répartir les unités au mieux pour éviter d'avoir trop de contraintes d’appariement problématiques. Le second pipeline n'est pas identique au pipeline originel, sur plusieurs points. Premièrement, il n'a pas exactement les mêmes circuits. Le second pipeline est généralement composé d'une unité de calcul, et permet d'exécuter une seconde opération de calcul, pas plus. En conséquence, les contraintes d'appariement assez fortes. Et il n'a pas forcément la même longueur, il peut avoir des étages en moins ou en plus. Il s'agit de la forme la plus simple de processeur superscalaire et nous allons leur consacrer cette section. Une autre implémentation utilise de la double émission, mais avec 3, 4, 5, ... ports d'émission. Le processeur ne donne plus l'impression d'avoir deux pipelines séparés, mais gagne en flexibilité. Les contraintes d'appariement sont plus faibles, moins nombreuses, moins problématiques. Mais le cout en circuit n'est pas négligeable. Voici le tableau chronologique disant quel CPU utilisait quelle technique. * Les CPU en vert ajoutaient un second pipeline pour les opérations flottantes. * Les CPU en rouge ajoutaient un second pipeline pour les opérations entières. * Les CPU en jaune utilisaient un ''partitionnement complet'' entre ALU, FPU et unité mémoire. * Les CPU sans couleur utilisaient un grand nombre d'unités, avec beaucoup de ports d'émission. * Les CPU barrés utilisent la triple ou quadruple émission et seront vu dans la suite. {|class="wikitable" |- ! !! IBM !! HP PA-RISC !! SPARC !! Intel!! Alpha !! Autres |- ! 1989 | class="f_vert | RS/6000 || || || class="f_rouge | i960CA || || |- ! 1990 | || || || || || |- ! 1991 | || || || || || Motorola 88110 |- ! 1992 | class="f_vert | RSC || class="f_vert | PA 7100 || <s>SuperSPARC</s> || || class="f_jaune | 21064 || |- ! 1993 | class="f_vert | PPC 601 || || class="f_jaune | HyperSPARC || class="f_rouge | Pentium || || |- ! 1994 | <s>POWER 2</s> || PA 7200 || || || <s>21164</s> || |} ===La double émission entière-flottante=== L'exemple le plus simple utilise un second pipeline pour les opérations flottantes, séparé du pipeline originel. L'ajout d'un pipeline flottant permet d'émettre une micro-opération entière en même temps qu'une micro-opération flottante, ce qui s'appelle la '''double émission entière-flottante'''. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Les exemples les plus notables sont le PA-RISC 7100, le POWER 1 (RIOS .& et .9) et le PowerPC 601. L'Apollo DN10000 et l'Intel i860 utilisaient aussi cette forme de double émission, mais étaient des prototypes de CPU VLIW. [[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]] Pour bien expliquer les choses, prenons le processeur illustré ci-dessous, avec deux ''back-end'' séparés : un pour la FPU, et un autre pour le reste. Les instructions entières et mémoire passent toutes par le ''back-end'' généraliste. [[File:CPU non-superscalaire basique, avec une FPU.png|centre|vignette|upright=1.5|CPU non-superscalaire basique, avec une FPU]] Les deux ''back-end'' sont des pipelines séparés, mais il faut que l'unité d'émission soit spécialement conçue pour en profiter. Un processeur simple émission peut démarrer une seule µop par cycle : soit la µop est envoyée au ''back-end'' flottant, soit elle est envoyée au ''back-end'' général. Avec la double émission entière-flottante, le processeur peut envoyer une µop dans chaque pipeline, à chaque cycle. Vu que la FPU et son banc de registre forment déjà un pipeline séparé, les seules modifications se font au niveau du ''front-end'', unité d'émission inclue. {| |- |[[File:CPU simple émission avec une FPU.png|centre|vignette|upright=1.5|CPU simple émission avec une FPU.]] |[[File:Double émission entière flottante avec un second pipeline.png|centre|vignette|upright=1.5|CPU avec un second pipeline flottant.]] |} Notez que le terme double émission ''entière''-flottante est un peu trompeur. En réalité, la technique permet d'émettre une instruction flottante avec n'importe quelle autre instruction non-flottante, avec des restrictions pour les branchements. Il est par exemple possible d'exécuter un calcul flottant en parallèle d'un accès mémoire. Sur les CPU 32 bits, il est possible de faire les multiplications dans la FPU. Pour rappel, la FPU contient un multiplieur entier 53-54 bits, pour multiplier les mantisses de deux flottants 64 bits, qu'on peut utiliser pour faire les multiplications entières 32 bits. Cela permet d'émettre une multiplication en même temps qu'une opération entière dans l'ALU. Ainsi, le CPU pouvait émettre deux instructions : une opération entière de base, avec soit une multiplication, soit une opération flottante. En clair, une émission de type INT + (MUL /FLOAT). Par contre, on perd la possibilité d'émettre une multiplication entière avec une opération flottante, mais c'était un compromis qui en valait la peine. Le CPU PA-RISC 7100 implémentait cette optimisation. Il s'agissait d'un CPU double émission entière-flottante, qui utilisait donc un partitionnement assez simple. Le combo double émission entière-flottante + multiplication dans la FPU permettait ici de gagner en performance, pour un cout en matériel très limité. D’autant plus limité que le jeu d'instruction prévoyait le reconditionnement de la FPU. Les multiplications ne lisaient pas leurs opérandes dans les registres entiers, mais dans les registres flottants. ===La double émission entière=== Une autre possibilité ajoute un second pipeline, capable d'exécuter des opérations entières simples, appelé le pipeline entier. Le processeur supporte alors la '''double émission entière''', à savoir qu'ils peuvent émettre une opération entière en plus d'une autre instruction. L'opération entière en question est une opération simple, comme une addition, une soustraction, une opération bit à bit, une comparaison, etc. Un exemple de CPU double émission de ce type est celui du Power PC 440, un CPU destiné à l'embarqué. Vu qu'il était destiné à l'embarqué, il n'avait pas de FPU, il n'avait pas besoin de manipuler de nombres flottants. Il n'a donc que des ALU entières, un multiplieur et une unité mémoire. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. Pour résumer : une ALU entière par port, l'unité mémoire sur un port, le multiplieur sur l'autre. Une organisation simple, mais très efficace ! [[File:PowerPC 440.png|centre|vignette|upright=2|Micro-architecture du PowerPC 440.]] Le processeur précédent n'avait pas de FPU, ce qui fait qu'il n'avait pas grand chose à connecter sur ses ports d'émission. le partage des ports d'émission était assez simple. Aussi il est intéressant de voir des exemples avec une FPU. Les exemples classiques sont les processeurs Intel i960 et Pentium. L'implémentation était différente entre l'i960 et le Pentium : Le Pentium avait une seconde ALU entière, alors que le i960 rusait. Le Pentium dupliquait l'ALU entière, pour créer un second pipeline entier. Sur le Pentium originel, les contraintes d'appariement étaient assez strictes. Le Pentium avait une FPU, mais ne pouvait pas l'utiliser en double émission. Il lui était impossible d'exécuter une opération flottante en même temps qu'une opération entière (branchements inclus). Le Cyrix 6x86 était une sorte de Pentium produit par l'entreprise Cyrix, avec une micro-architecture différente. Il avait des contraintes d’appariement réduites. Par exemple, il pouvait émettre une opération flottante en parallèle d'une opération entière. Nous détaillerons ces processeurs dans le prochain chapitre, qui porte justement sur les CPU superscalaires x86. [[File:CPU simple et double émission avec un second pipeline entier.png|centre|vignette|upright=2.5|CPU simple et double émission avec un second pipeline entier.]] L'Intel i960 n'avait pas de FPU, juste une ALU entière et une unité mémoire. Il utilisait l'unité mémoire comme une seconde ALU entière limitée. Pour rappel, une unité mémoire contient une AGU, à savoir une ALU spécialisée dans les calculs d'adresse. Elle est capable de faire des additions, soustractions, et quelques décalages limités (des décalages par 2, 4, 8, pas plus). L'Intel i960 ajoutait des connexions entre AGU et banc de registre, afin que celle-ci serve de seconde unité de calcul. L'AGU est déjà reliée aux deux ports de lecture, pour lire les opérandes du calcul d'adresse, il suffit de la connecter sur le port d'écriture. Le processeur peut alors émettre deux instructions/µops entières, sous certaines conditions. [[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission.]] ===La double émission INT-FLOAT-MEM=== La '''double émission INT-FLOAT-MEM''' utilise la technique du partitionnement superscalaire, vue plus haut, avec cependant une petite subtilité. Elle utilise précisément une ALU entière, une FPU et une unité mémoire. En théorie, le partitionnement devrait donner un CPU triple émission, pas double émission. Et c'est là que la différence entre ports d'émission et de décodage devient intéressante. L'idée est d'avoir une unité d'émission avec deux ports de décodage, et 2 ports d'émission (un par unité). Les contraintes d’appariement varient alors suivant le processeur, mais la règle générale est que si deux micro-opérations tombent dans deux unités différentes, on peut les émettre en même temps. En clair, on peut émettre une opération entière avec une opération flottante, une opération entière avec un accès mémoire, une opération flottante avec un accès mémoire. Et cela rend le partitionnement bien plus performant que prévu. L'avantage sur la double émission flottante est que les combinaisons INT + MEM et FLOAT + MEM étaient autorisées. Le processeur pouvait donc émettre une opération en parallèle d'un accès mémoire, ce qui est un doublet d'instruction assez fréquent. Le gain en performance par rapport à la doublé émission entière-flottante était substantiel. Un détail est que cette forme de double émission ne peut pas s'interpréter comme un processeur à deux pipelines. Le processeur n'a pas deux ports d'émission, mais trois, et les contraintes d'appariement collent assez mal avec deux pipelines séparés. Il est possible d'interpréter le tout comme un CPU à trois pipelines, mais qui n'autorise que les paires d'instructions, mais ce serait assez tiré par les cheveux. Aussi, la notion de "plusieurs pipelines indépendants" ne marche pas pour décrire ce genre de processeurs superscalaires. Leur nombre de voies reste de deux, mais le nombre de pipelines est indéterminé. Les exemples classiques sont l'HyperSPARC, l'alpha 21064 et le PowerPC 603. Le PowerPC 603 implémentait une double émission "parfaite". C'est à dire que toutes les combinaisons possibles à deux instructions étaient possibles, que ce soit INT + FLOAT, INT + MEM, FLOAT + MEM. En plus de cette double émission "parfaite", le CPU utilisait la technique dite de ''branch folding'', qui permettait d'exécuter un branchement sans utiliser d'unité de calcul, ce qui fait qu'il était qualifié de ''CPU 2-way + Branch''. A l'opposé, le processeur alpha 21064 avait pas mal de contraintes d’appariement, à savoir que certaines combinaisons de deux instructions étaient interdites, bien que compatibles avec les unités disponibles. Une des raisons est que le banc de registre n'avait pas assez de ports de lectures, ce qui fait que certaines combinaisons avec une écriture mémoire n'étaient pas permises. Les concepteurs avaient volontairement inséré ces contraintes afin de simplifier le design du processeur. ===La double émission avec partitionnement et duplication d'ALU entière=== Nous venons de voir qu'il est possible d'utiliser efficacement un surplus de ports d'émission, afin de réduire les contraintes d'appariement. Des ports d'émission qui étaient partagés sont simplement dupliqués et les ALU sont réparties sur des ports d'émission différents. Il est possible de pousser cette logique encore plus loin, en utilisant un grand nombre d'unité de calcul. L'exemple historique le plus impressionnant est celui du Motorola 88110, un CPU double émission qui possède pas moins de 10 unités ! Il contient deux ALU entières, un circuit multiplieur, un circuit diviseur, une unité de manipulation de bits, une unité mémoire, un additionneur flottant, et deux unités graphiques pour des calculs impliquant des pixels. Le multiplieur est utilisé à la fois pour les multiplications entières, flottantes et le produit de deux pixels. Idem pour le diviseur, qui sert pour les divisions entières et flottantes. Et le partitionnement est poussé au point où chaque unité a son propre port d'émission. La conséquence est que les contraintes d'appariement du Motorola 88110 sont simples : tant que deux µops atterrissent dans des unités séparées, la paire est permise. Il est par exemple possible d'émettre simultanément deux opérations entières simples, vu qu'il y a deux ALU. Ou encore, d'émettre une addition et une multiplication, une multiplication et un décalage, un décalage et une division, une addition entière et une addition flottante, etc. Par contre, impossible d'émettre simultanément deux multiplications, deux divisions, deux opérations flottantes, deux manipulations de bit, deux accès mémoire, etc. De plus, le partage du multiplieur fait qu'on ne peut pas émettre une multiplication entière avec une multiplication flottante. Idem avec les divisions, vu que le diviseur est aussi partagé. Ce sont les seules contraintes, et c'est très peu comparé aux autres processeurs double émission. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le parallélisme mémoire | prevText=Le parallélisme mémoire | next=Les processeurs superscalaires out-of-order | nextText=Les processeurs superscalaires out-of-order }} </noinclude> f95ypkafxrdjn4jz8vyzfzvvlqaosvt Fonctionnement d'un ordinateur/Les mémoires cache 0 65957 773891 773813 2026-10-01T17:29:48Z Mewtow 31375 773891 wikitext text/x-wiki La '''mémoire cache''' est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Sans elle, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire RAM est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instructions (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente. Le cache est fabriqué avec de la mémoire SRAM, avec quelques exceptions qui utilisent de l'eDRAM. Les tout premiers processeurs utilisaient un cache externe, séparé du processeur, qui était installé sur un connecteur dédié. Mais de nos jours, le cache est intégré dans le processeur, directement. La loi de Moore a permis d'utiliser des transistors pour le cache, en plus des cœurs du processeur. Et le gain de cette intégration est assez clair : le cache est plus rapide quand il est dans le processeur que dehors. Mais même l'intégration du cache dans le processeur ne suffit plus de nos jours. Les processeurs sont devenus tellement rapides qu'ils ne peuvent plus se contenter d'un seul cache ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle. Et qui dit même problème, dit même solution ! Les processeurs modernes utilisent une hiérarchie de mémoires caches, allant d'un cache très petit mais très rapide, à un cache plus lent mais de grande capacité. Les caches en question sont appelés le '''cache L1''' pour le plus petit et le plus rapide, le '''cache L2''' pour le cache intermédiaire, et le '''cache L3''' pour le cache gros et lent. [[File:Cache Hierarchy.png|centre|vignette|upright=1.5|Hiérarchie de caches]] Les processeurs assez anciens n'utilisaient pas de cache L3, ils avaient juste un cache L1 petit et rapide, et un cache L2 moyen en tout. [[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]] Le cache L1 est le cache le plus proche du processeur. La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un '''cache L1 de données''' pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions. La séparation entre cache L1 de données et d’instruction est apparue en même temps que la scission entre cache L1 et L2. Le cache L2 mémorise indifféremment données et instructions. [[File:Cache d'instructions.png|centre|vignette|upright=2|Cache d'instructions.]] Le cache le plus proche de la mémoire est appelé le '''cache de dernier niveau''', ''Last Level Cache'' en anglais. Concrètement, c'est le cache L2 sur les anciens processeurs, le cache L3 ou L4 sur les processeurs modernes. Le cache L3/L4 a parfois des caractéristiques totalement différentes des autres caches. Par exemple, sur les processeurs multicœurs, le cache L3 n'est techniquement pas dans le cœur, mais fait partie du contrôleur mémoire ou de l'interface mémoire. Il fonctionne à une fréquence différente du processeur, n'a pas la même tension d'alimentation, etc. Le nombre de niveaux de cache varient d'un processeur à l'autre. Les tout premiers processeurs faisaient avec un cache unique, sans hiérarchie de cache. Mais par la suite, la scission entre L1 et L2 est apparue. Puis un cache L3 a été ajouté, avec parfois un cache L4. Pour rester général, la hiérarchie de caches est composée de plusieurs '''niveaux de cache''', qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides. Petit point terminologie avant de poursuivre : j'utiliserais parfois l'expression "niveaux de cache inférieurs". L'expression se fait quand on parle d'un niveau de cache bien précis, souvent le niveau L1. Quand on parle des "niveaux de cache inférieurs au L1", cela veut dire le cache L2 et L3, voire ceux en-dessous s'ils existent. Idem pour les "niveaux de cache inférieurs au L2", qui désigne le cache L3. La mémoire RAM est souvent implicitement comptée dans cette expression, même si elle n'est pas un cache à proprement parler. ==L'accès au cache== Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur. Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard. [[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]] La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là. [[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]] ===Les succès et défauts de caches=== Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM. Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général. La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas). Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours. ===Le fonctionnement du cache, vu du processeur=== Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets. Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés. Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés. [[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]] ===LA situation avec une hiérarchie de caches=== Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas, on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire. [[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]] ==La performance des mémoires caches== L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache. ===Le taux de succès/défaut=== Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à : : <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math> Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à : : <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math> Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas. Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes. Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres. Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena. {{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}} ===La latence moyenne d'un cache=== Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>. En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors : : <math>T = T_c + \text{Taux de défaut} \times T_m</math> On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs. Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants). ===L'impact de la taille du cache sur le taux de défaut et la latence=== Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches. Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge. Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi : : <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté. Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache. L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment : : <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations. Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations. Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide. La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long. ==Les lignes de cache et leurs tags== Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes. ===Les lignes de cache=== Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM. En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille. Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples. ===L'alignement des lignes de cache=== Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne. Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache. L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants. L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Si une données, comme un entier ou un flottant, est à cheval sur deux lignes de cache, elle sera chargée en deux fois, en deux lectures. idem pour les écritures. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique. ===Le tag d'une ligne de cache=== Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''. Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante. [[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]] Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée. [[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]] ===Le contenu d'une ligne de cache=== Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré. [[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]] Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''. Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache. Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''. Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre. Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs. [[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]] : Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples. ==Les instructions de contrôle du cache== Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation. ===Les instructions de préchargement=== La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse. L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique. Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles. ===Les instructions d'invalidation et de ''flush''=== Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions. Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache. Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute. Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. À la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point. Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2. Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''. ===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées=== Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache. D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''. L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles. Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire. Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc. ==L'associativité des caches et leur adressage implicite== Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie. ===Les caches totalement associatifs=== Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches. [[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]] Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible. [[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]] Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur. [[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]] Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM. [[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]] Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs. [[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]] ===Les caches directement adressés=== Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion. [[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]] Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer. Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous. [[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]] Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache. [[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]] Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire. [[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]] L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches. [[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]] ===Les caches associatifs par voie=== Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie. [[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]] Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''. [[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]] Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs. [[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]] Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits. ==Les optimisations des caches associatifs par voie== Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations. ===Les caches pseudo-associatifs=== Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370. Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie. L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache. ===La prédiction de voie=== Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité. Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie. Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste. Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags. Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée : * soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ; * soit par l'adresse à accéder (là encore, quelques bits de poids faible) ; * soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ; * soit par autre chose. ===La mise en veille sélective des voies=== Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes. Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue. ===Les caches ''skew-associative''=== Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais). Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie. [[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]] ==L'adressage physique ou logique des caches== Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second. {| |[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]] |[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]] |} ===L'accès à un cache physiquement/virtuellement tagué=== La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds. Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides. Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs. [[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]] ===Les défauts des caches virtuellement tagués=== Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes. ====Les droits d'accès doivent être vérifiés lors d'un accès au cache==== Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs. Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable. ====Les adresses homonymes perturbent la gestion du cache==== Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser. Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur. Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur. Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors. L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU. ====Les adresses synonymes perturbent aussi la gestion du cache==== La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare ! Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes. ===Les caches virtuellement adressés, mais physiquement tagués=== Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies). L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse. Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache. L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''. [[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]] Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice. La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance. ==Le remplacement des lignes de cache== Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps. Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples. Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace. De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache. ===Le remplacement aléatoire=== Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat. ===FIFO : first in, first out=== Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire. [[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]] Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''. ===MRU : most recently used=== Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée. Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables. ===LFU : least frequently used=== Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur. [[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]] ===LRU : least recently used=== Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles. Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU. ===Les approximations du LRU=== Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants. L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies. Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement. Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture. {| |[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]] |[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]] |} ===LRU amélioré=== L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent. Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur. D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation. ==Les écritures dans le cache : gestion et optimisations== Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''. Avec un '''cache ''write-back''''', si une écriture a lieu dans une ligne de cache, elle ne concerne que ce cache. Elle n'est pas propagée dans les niveaux de cache inférieurs, ni en RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles. [[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]] Avec les '''caches ''Write-Through''''', toute écriture dans le cache est propagée dans les niveaux de cache inférieurs s'ils existent, puis en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont beaucoup plus simples à implémenter. Ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs. [[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]] ===Les caches ''Write-through''=== Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps. Cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache. Et le même problème survient quand les niveaux de cache inférieurs sont utilisés en écriture. L'écriture ne peut pas être propagée tant que le port d'écriture est occupé. Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures. C'est une mémoire FIFO dans laquelle on place temporairement les écritures, à savoir : les données à écrire en RAM, mais aussi l'adresse à laquelle écrire. Les écritures attendent dans le tampon d'écriture que les niveaux de cache inférieurs (ou la RAM) soient libres. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse sont fusionnées en une seule, dans le tampon d’écriture. Cela fait un peu de place dans le tampon d’écriture, ce qui lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''. L'usage d'un tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. Mais on peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO. ===Les caches ''Write-back''=== Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM. En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer''). [[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]] Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Le cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus. [[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]] Quelques processeurs des années 2010 avaient un cache L3 qui était un cache de victime. Les défauts à froid chargeaient la donnée associée dans le cache L2, plus rarement dans le cache L1 directement. En tout cas, les données ne passaient pas par le cache L3. Le seul moyen pour qu'une donnée atterrissent dans le cache L3 est qu'elles aient été évincées du cache L2. C'était une solution adaptée pour une époque où avoir un gros cache était primordial, mais où le budget en transistor était un peu serré. Faire du L3 un cache de victime économisait des transistors, qui pouvaient être utilisés pour faire grossir le cache. De nos jours, les budgets en transistors permettent de faire autrement. ===La configuration du fonctionnement du cache=== Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture. Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau. Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée. Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe. De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec. ===L’allocation sur écriture=== Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement. L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''. [[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]] Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs. [[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]] ===La cohérence des caches=== Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache. Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour. ==Les caches exclusifs et inclusifs== Au début du chapitre, nous avons abordé le fait qu'un processeur moderne intégre une hiérarchie de mémoire caches, avec un cache L1, un cache L2 et un cache L3. Il est intéressant de regarder comment sont réaprties les données dans ces différents niveaux de cache. Et cela nous amène à parler des caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple. {| |[[File:Exclusivecache.png|vignette|upright=0.5|Cache exclusif.]] |[[File:Inclusivecache.png|vignette|upright=0.5|Cache inclusif.]] |} Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps. [[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]] Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches. [[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]] Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème. Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds. Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques. ==Les caches eDRAM, sur la carte mère et autres== D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement. [[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]] Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. On parlait alors de '''''Cache on a stick''''' (COAST). On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. Les ''Cache on a stick'' ont été introduits sur les processeurs Motorola, et utilisés sur les IBM PC et les Macintosh de l'époque. Les ordinateurs Macintosh utilisaient de tels caches, pour la plupart des modèles. Les processeurs PA RISC de HP utilisaient aussi de tels caches déportés hors du processeur. Sur les modèles comme le 7100, le processeur n’intégrait aucun cache et devait se se débrouiller avec un unique cache situé hors du CPU. Derrière ces choix se cachait une volonté d'utiliser un maximum de transistors pour les unités de calcul et de contrôle. Le cache prenait de la place sur le ''die'', dont ils en déportaient un maximum hors du processeur. Les modèles ultérieurs, comme le PA 7300, avaient le budget en transistor pour intégrer des caches. Ils intégraient des caches L1 de données et d'instructions, mais le L2 était déporté hors du CPU. Les caches L1 étaient cependant assez larges, comparé à la moyenne de l'époque. Le choix d'HP était d'avoir un unique niveau de cache, optimisé de manière à pouvoir être à la fois rapide et de grande capacité. Pour ce qui est des PC, les premiers processeurs x86 faisaient pareil, notamment les processeurs Intel. Le 486, le Pentium et le Pentium 2 utilisaient des ''Cache on a stick''. L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner soit sans mémoire cache, soit avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin. Pour les CPU Intel, le cache était connecté sur le bus système, au même titre que la mémoire RAM et les entrées-sorties. Il faut dire que les processeurs de l'époque utilisaient un bus système et n'avaient pas de bus mémoire dédié. Mais en théorie, rien n’empêche de connecter le cache sur un bus mémoire dédié. Toujours est-il que les lectures et écritures étaient propagées à la fois dans le cache et la RAM. Les écritures se faisaient dans les deux, systématiquement dans la RAM, mais aussi dans le cache en cas de succès de cache. Les lectures étaient servies soit par le cache en cas de succès de cache, soit par la RAM en cas de défaut de cache. Si le cache répondait en premier, la transaction sur le bus se terminait précocement et l'accès en RAM était abandonné. [[File:Intel486 Иерархия памяти.png|centre|vignette|upright=2.5|Intel486 : le cache était connecté sur le bus système.]] À l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO. Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, que nous allons voir dans la section suivante. ==Le ''cache bypassing'' : contourner le cache== Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant. ===Accéder aux périphériques demande de contourner le cache=== Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire. La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes. Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si. Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable. Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci. ===Contourner le cache pour des raisons de performance=== Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard). Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache. L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non. ===Le cas particulier du processeur MIPS R8000=== Le processeur MIPS R8000 intégrait une forme de ''cache bypassing'', qui est, à ma connaissance, totalement inédite. Et pour la comprendre, faisons un léger rappel. Les processeurs qui supportent les nombres flottants disposent d'instructions de lecture/écriture spécialisées pour les nombres flottants, séparées des instructions LOAD et STORE classiques. Elles sont souvent appelées FLOAD et STORE. Les fait de séparer les instructions LOAD/STORE des instructions FLOAD/FSTORE permet d'implémenter des optimisations, ce qui est le cas ici. Sur le MIPS R8000, les instructions FLOAD et FSTORE contournaient le cache, à savoir que les flottants étaient lus/écrits sans passer par la mémoire cache. La raison à cela est que les applications de l'époque avaient une bonne localité spatiale et temporlle pour les instructions LOAD et STORE classiques, mais une très mauvaise localité spatiale et temporelle pour leurs équivalents flottant. Concrètement, les applications faisant beaucoup de calculs flottants profitaient peu de la présence de mémoire cache. Si les flottants passaient dans le cache, ils pollueraient le cache, à savoir qu'ils remplaceraient des données bien plus utiles. Les choses sont cependant compliquées par la hiérarchie de cache un peu bizarre de ce processeur. Le processeur MIPS R8000 avait deux caches L1, un pour les données et un pour les instructions. De plus, il pouvait utiliser un cache L2 externe, placé sur la carte mère. Les instructions LOAD et STORE ne contournaient pas les caches L1 et L2, ce qui fait que les nombres entiers et adresses pouvaient sans problème être copiés dans le cache L1 de données. Les instructions FLOAD et FSTORE ne passaient pas par le cache L1, mais atterrissaient dans le cache L2. En clair, le cache L1 de données était contourné, mais pas le cache L2. Toute lecture/écriture flottante se faisait dans le cache L2 externe, elle contournait automatiquement le cache L1. Comme toute forme de ''cache bypassing'', le MIPS R8000 devait faire face à des problèmes de cohérence des caches, qui apparaissaient des des situations bien spécifiques. Les problèmes survenaient quand des entiers et des flottants étaient mixés entre dans dans une même structure de donnée (voire quand un mot contenait indifféremment entier et flottant, grâce à l'usage d'unions). Une écriture flottante pouvait modifier un mot mémoire, sans que la copie dans le cache L1 soit modifiée. Idem avec une écriture entière, qui pouvait modifier un mot dans le cache L1, sans que le cache L2 soit au courant. Pour résoudre le problème, les écritures flottants étaient partiellement prises en compte par le cache L1. Le flottant écrit n'était pas écrit dans le cache L1, mais l'adresse de l'écriture était envoyée au cache L1, pour que celui-ci vérifie s'il détient une copie de cette adresse. Si c'est le cas, la ligne de cache correspondante était invalidée. Pour gérer ces invalidations, chaque ligne de cache était associée à un bit de validité, qui était mis à 1 lorsque la donnée était chargée depuis la mémoire RAM. Les écritures flottantes mettaient à 0 ce bit de validité, les écritures entières le remettaient à 1. ==Les caches splittés (''phased caches'')== Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Nous allons partir du principe que ce sont des caches ''direct-mapped'', mais l'optimisation peut être utilisée sur un cache associatif par voie. L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs. Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache. Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin. L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle. [[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]] Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres. Le processeur UltraSPARC III utilisait un cache splitté particulier, pour son cache L2. Son cache L1 était un cache normal, non(-splitté. Le cache était splitté : les ''tags'' étaient dans le processeur, alors que les données étaient dans un cache situé en-dehors du processeur, sur la carte mère. Faire ainsi permettait de déporter beaucoup de transistors en-dehors du processeur, tout en conservant des caches performants. Le fait d'intégrer les ''tags'' dans le processeur faisait que consulter les ''tags'' était très rapide, accéder aux données était plus lent. En contrepartie, la SRAM des données était bien plus grosse. ===Le contrôleur de cache 82385 pour les CPU Intel 386=== Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Voire que les deux sont séparés du processeur ! C'était le cas quand les mémoires caches ont été introduites sur les processeurs grand public, notamment sur les premiers processeurs Intel. La miniaturisation n'avait pas avancé au point où placer un cache dans le processeur était possible. Sur le processeur 386 d'Intel, le cache était un cache splitté, séparé du processeur. Concrètement, le processeur i386 était couplé à un contrôleur de cache Intel 82385 et une mémoire SRAM. Le 82385 contenait les ''tags'' et les bits de contrôle, la SRAM contenait les données, les lignes de cache. Un point important est que les lignes de cache faisaient seulement 32 bits/4 octets, pas plus ! On était loin des lignes de cache actuelles, faisant 64 octets/512 bits. Mais c'était beaucoup plus pratique, vu que le bus système faisait 32 bits de large, idem pour l'interface avec le processeur. Le schéma ci-dessous montre comment le cache s'intégrait avec le bus système. Pour le bus de commande, le cache servait d'intermédiaire : il recevait les commandes et les filtrait suivant les succès/défauts de cache. En cas de succès de cache, les commandes de lecture n'étaient pas envoyées à la mémoire RAM. Les adresses étaient transmises à la fois au cache et au bus système (avec un registre entre le bus système et le processeur). Le bus de donnée était lui connecté à la mémoire SRAM et au processeur, avec des MUX/DEMUX pour faire le choix de la source des lectures. [[File:Controleur de cache 82385 pour l'Intel 386.png|centre|vignette|upright=2.5|Contrôleur de cache 82385 pour l'Intel 386]] Le 82385 surveillait ce qui se passait sur le bus et répondait à la place de la RAM pour certaines lectures. C'était un intermédiaire assez passif, qui se contenait de répondre aux succès et défauts en lecture. Le cache était un cache ''write through'' un peu particulier. En cas de succès de cache pour une écriture, le cache met à jour sa ligne de cache et propage l'écriture en mémoire RAM. Par contre, si une écriture fait un défaut de cache, la donnée n'est pas écrite dans le cache. Le seul moyen pour copier une donnée dans le cache était un défaut pour une lecture. Le 82385 pouvait commander soit un cache ''direct mapped'', soit associatif à deux voies. Le choix entre les deux était le fait d'une entrée : la mettre à 0 indiquait un cache ''direct mapped'', la mettre à 1 forçait un cache à deux voies. La différence entre les deux est que le 82385 était relié à une mémoire SRAM avec un cache ''direct mapped'', deux SRAM pour deux voies. Pour avoir un cache associatif à deux voies, le 82385 devrait gérer deux signaux ''chip select'' pour activer chaque SRAM/voie suivant les besoins. Il avait précisément quatre signaux CS : deux par SRAM, un pour les lectures, un pour les écritures. Notons que les lignes de cache faisaient 32 bits, ce qui pouvait d'obtenir soit avec une SRAM 32 bits, soit avec deux SRAM 16 bits, soit avec 4 SRAM 8 bits. Le 82385 rajoutait 4 sorties, pour masquer chaque octet dans ces 32 bits, qui sont techniquement des signaux ''Output Enable'' pour 4 SRAM 8 bits. [[File:Interface entre le 82385 et la SRAM du cache.png|centre|vignette|upright=2|Interface entre le 82385 et la SRAM du cache. Beaucoup d'entrées et de sorties liées au bus d'adresse ne sont pas représentées.]] Il gérait aussi les accès mémoire non-cacheable, à savoir des accès mémoire qui ne doivent pas être pris en compte par le cache. Il considérait certains accès mémoire comme "à ne pas cacher". Notamment, les accès mémoire à une entrée-sortie ne sont pas cachés. Pour rappel, le processeur utilisait un espace d'adressage séparé pour les entrées-sorties, et utilisait donc un bit IO, qui était utilisé par le 82385 pour savoir si l'accès mémoire doit être caché ou non. Il en est de même pour les accès ayant lieu lors d'une interruption, qui ne passent pas par le cache. Mais au-delà de cette inhibition automatique du cache, le 82385 avait une entrée NCA (''Non Cacheable Access'') : le cache était "désactivé" quand cette entrée était à 1. C'est un peu une sorte de ''chip select'' pour le 82385, limitée aux accès mémoire. Cette entrée permettait de programmer des intervalles d'adresse auxquels ne pas répondre, en utilisant des circuits de décodage d'adresse adaptés. Il avait aussi une entrée X16, qui permettait d'identifier les accès soit à un composant 16 bits. De tels accès ne doivent pas être mis en cache, sans doute parce que cela ne collait pas avec la taille des lignes de cache (32 bits). Et cette entrée permettait d'inhiber ces accès 16 bits d'agir sur le cache, en utilisant le bit du bus de commande adéquat. Le 82385 pouvait être intégré dans un système à deux processeurs, voire plus. Pour cela, chaque processeur avait son propre 82385 et sa SRAM rien qu'à lui. Il n'y avait pas de cache partagé entre les deux processeurs. Par contre, les deux caches étaient reliés au même bus système. Pour qu'ils ne se marchent pas sur les pieds, il y avait des circuits d'arbitrage pour gérer l'accès au bus. Un des deux 82385 était mis en mode maitre, l'autre était en mode esclave. Le 82385 maitre pouvait prendre le contrôle du bus, le 82385 esclave devait demander l'autorisation au premier pour accéder au bus système. Le 82385 gérait une forme limitée de cohérence des caches par invalidation. Dès que le 82385 détectait une prise de contrôle du bus par autre chose que le processeur, il surveillait les adresses transmises sur le bus. En cas de succès de cache, la ligne de cache associée était invalidée. Au-delà de ça, le 82385 avait une entrée FLUSH, qui ordonnait une invalidation totale du cache. Si cette entrée est mise à 1, toutes les lignes de cache sont invalidées. Les ''tags'' sont marqués comme invalides, mais les lignes de cache elles-mêmes ne sont pas touchées. ===L'exemple des processeurs Intel de microarchitecture ''Broadwell''=== Un autre exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO ! La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes. Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. À chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données. Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags. ===Les caches RAM-configurables=== Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable. [[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]] ===La compression de cache=== Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs. Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache). Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ? [[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]] Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée. Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux : * [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec]. ==Les caches adressés par somme et hashés== Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme. Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement. Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse. [[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]] Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante. [[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]] Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit : : <math>A + B = K</math> Ce qui est équivalent à faire le test suivant : : <math>A + B - K = 0</math> En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a : : <math>A + B + \overline{K} + 1 = 0</math> En réorganisant les termes, on a : : <math>A + B + \overline{K} = - 1</math> Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a: : <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>. Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux. : <math>S + (R << 1) = 111 \cdots 111111</math> [[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]] Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors : : <math>S \oplus (R << 1) = 111 \cdots 111111</math> La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester. Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances. [[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]] En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable. ==Les caches à accès uniforme et non-uniforme== Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres. Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme. [[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]] Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme. Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe. Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant. Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA''). Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique. ==La tolérance aux erreurs des caches== Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens. ===Les mémoires caches ECC et à bit de parité=== Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé. Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits. La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''. Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC. ===L'usage du ''memory scrubbing'' sur les caches=== La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance. Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire. Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable. ===Le cas particulier des caches ''Write-Through''=== Les caches ''Write-Through'' et ''Write-Back'' ne sont pas égaux du point de vue de l'ECC. En effet, les caches ''Write-Back'' ont un désavantage : quand une donnée est modifiée dans le cache, seul le cache a une copie de la donnée valide. Ils ont donc intérêt à protéger cette donnée contre les erreurs, ce qui implique l'usage de l'ECC au minimum. A l'opposé, les caches ''Write-Through'' n'ont pas ce problème : ils garantissent que toute donnée dans le cache est aussi recopiée ailleurs, que ce soit en mémoire RAM, dans le niveau de cache inférieur, ou dans un ''write buffer''/tampon d'écriture. Ils peuvent donc se contenter d'utiliser un bit de parité par mot, voire par ligne de cache. Ce qui entraine des économies substantielles de circuits. En général, les processeurs préfèrent utiliser des caches ''Write-Back'' pour tous les niveaux, L1, L2 et L3, voire L4. Mais quelques rares processeurs préféraient utiliser des ''Write-Through'' pour le niveau L1. Je précise bien : uniquement pour le niveau L1, pas pour les niveaux L2 et L3. Les niveaux L2 et L3 utilisaient des caches ''Write-Back'' protégés avec de l'ECC, seuls les caches L1 se débrouillaient sans. Un exemple est le Pentium 4 d'Intel, qui se débrouillait avec un L1 ''Write-Through'', sans tampon d'écriture. Les performances en écritures étaient de fait très mauvaises, du fait de l'absence de tampon d'ériture. Les processeurs AMD de microarchitecture Bulldozer avaient eu aussi un cache L1 ''Write-Through'', mais combinés avec un tampon d'écriture, pour conserver des performances acceptables. Le tampon d'écriture en question était un cache de 4 kibioctets, à 4 voies. Les performances du cache en écriture étaient cependant inférieures à celles des processeurs concurrents, qui utailisaient des caches ''Write-Back'' ==Un exemple de cache : le cache d'instruction== [[File:Separate unified.png|thumb|Cache d'instruction séparé du cache de données.]] La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions. Les deux caches sont reliés au processeur par des bus séparés, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une '''architecture Harvard modifiée'''. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM. Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Le cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur. [[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]] ===Pourquoi scinder le cache L1 en cache d'instruction et de données=== L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache L1 multiport unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs avaient un cache L1 unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes. Le compromis à faire est celui entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. En termes de temps d'accès, la meilleure solution est celle des deux caches simple port. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. La différence est surtout notable pour les gros caches, moins pour les petits caches. Il faut aussi tenir compte de la capacité effective. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 KB et un cache de données de 32 KB, impossible d'allouer 40 KB aux données et 20 aux instructions. Alors qu'avec un cache L1 unique de 64 KB, on pourrait le faire sans soucis. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre. Tout cela explique pourquoi le cache L1 est le seul à être ainsi scindé en deux, avec une séparation entre instructions et données : les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Pour les caches L1, le temps d'accès est plus important que la capacité, ce qui favorise les caches séparés. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que la capacité effective et l'économie en circuits sont significatives. ===La connexion des caches L1 avec le cache L2=== Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément. [[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]] Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé. [[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]] Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre ! ===Les spécificités du cache d'instruction : lecture seule, bloquant, etc=== Les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans. Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés. Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour. Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale. Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. À l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches. ===L'impact du cache d'instruction sur les performances=== Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions. : La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale. La conséquence est qu'il arrive que certains CPU aient un cache L1 d'instruction plus gros que celui pour les données. On parle alors de '''cache L1 asymétriques'''. Un exemple est celui des processeurs AMD de microarchitecture Zen, dont le cache d'instruction était deux fois plus gros que le cache de données. Leur cache d'instruction faisait 64 kibioctets, contre seulement 32 pour le cache de données. D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Compléments sur les mémoires de masse | prevText=Compléments sur les mémoires de masse | next=Le préchargement | nextText=Le préchargement }} </noinclude> d6ajcvwepaq1hek657oi21bp6z3tl5t 773892 773891 2026-10-01T17:31:51Z Mewtow 31375 773892 wikitext text/x-wiki La '''mémoire cache''' est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Sans elle, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire RAM est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instructions (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente. Le cache est fabriqué avec de la mémoire SRAM, avec quelques exceptions qui utilisent de l'eDRAM. Les tout premiers processeurs utilisaient un cache externe, séparé du processeur, qui était installé sur un connecteur dédié. Mais de nos jours, le cache est intégré dans le processeur, directement. La loi de Moore a permis d'utiliser des transistors pour le cache, en plus des cœurs du processeur. Et le gain de cette intégration est assez clair : le cache est plus rapide quand il est dans le processeur que dehors. Mais même l'intégration du cache dans le processeur ne suffit plus de nos jours. Les processeurs sont devenus tellement rapides qu'ils ne peuvent plus se contenter d'un seul cache ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle. Et qui dit même problème, dit même solution ! Les processeurs modernes utilisent une hiérarchie de mémoires caches, allant d'un cache très petit mais très rapide, à un cache plus lent mais de grande capacité. Les processeurs assez anciens n'utilisaient pas de cache L3, ils avaient juste un cache L1 petit et rapide, et un cache L2 gros et lent. [[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]] Depuis les années 2010, les processeurs utilisent une hiérarchie à trois niveaux de cache. Les caches en question sont appelés le '''cache L1''' pour le plus petit et le plus rapide, le '''cache L2''' pour le cache intermédiaire, et le '''cache L3''' pour le cache gros et lent. [[File:Cache Hierarchy.png|centre|vignette|upright=1.5|Hiérarchie de caches]] La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un '''cache L1 de données''' pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions. Le cache L2 mémorise indifféremment données et instructions. La séparation entre cache L1 de données et d’instruction est apparue en même temps que la scission entre cache L1 et L2. [[File:Cache d'instructions.png|centre|vignette|upright=2|Cache d'instructions.]] Le cache le plus proche de la mémoire est appelé le '''cache de dernier niveau''', ''Last Level Cache'' en anglais. Concrètement, c'est le cache L2 sur les anciens processeurs, le cache L3 ou L4 sur les processeurs modernes. Le cache L3/L4 a parfois des caractéristiques totalement différentes des autres caches. Par exemple, sur les processeurs multicœurs, le cache L3 n'est techniquement pas dans le cœur, mais fait partie du contrôleur mémoire ou de l'interface mémoire. Il fonctionne à une fréquence différente du processeur, n'a pas la même tension d'alimentation, etc. Le nombre de niveaux de cache varient d'un processeur à l'autre. Les tout premiers processeurs faisaient avec un cache unique, sans hiérarchie de cache. Mais par la suite, la scission entre L1 et L2 est apparue. Puis un cache L3 a été ajouté, avec parfois un cache L4. Pour rester général, la hiérarchie de caches est composée de plusieurs '''niveaux de cache''', qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides. Petit point terminologie avant de poursuivre : j'utiliserais parfois l'expression "niveaux de cache inférieurs". L'expression se fait quand on parle d'un niveau de cache bien précis, souvent le niveau L1. Quand on parle des "niveaux de cache inférieurs au L1", cela veut dire le cache L2 et L3, voire ceux en-dessous s'ils existent. Idem pour les "niveaux de cache inférieurs au L2", qui désigne le cache L3. La mémoire RAM est souvent implicitement comptée dans cette expression, même si elle n'est pas un cache à proprement parler. ==L'accès au cache== Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur. Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard. [[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]] La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là. [[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]] ===Les succès et défauts de caches=== Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM. Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général. La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas). Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours. ===Le fonctionnement du cache, vu du processeur=== Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets. Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés. Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés. [[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]] ===LA situation avec une hiérarchie de caches=== Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas, on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire. [[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]] ==La performance des mémoires caches== L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache. ===Le taux de succès/défaut=== Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à : : <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math> Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à : : <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math> Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas. Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes. Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres. Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena. {{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}} ===La latence moyenne d'un cache=== Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>. En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors : : <math>T = T_c + \text{Taux de défaut} \times T_m</math> On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs. Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants). ===L'impact de la taille du cache sur le taux de défaut et la latence=== Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches. Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge. Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi : : <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté. Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache. L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment : : <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations. Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations. Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide. La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long. ==Les lignes de cache et leurs tags== Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes. ===Les lignes de cache=== Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM. En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille. Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples. ===L'alignement des lignes de cache=== Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne. Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache. L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants. L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Si une données, comme un entier ou un flottant, est à cheval sur deux lignes de cache, elle sera chargée en deux fois, en deux lectures. idem pour les écritures. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique. ===Le tag d'une ligne de cache=== Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''. Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante. [[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]] Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée. [[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]] ===Le contenu d'une ligne de cache=== Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré. [[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]] Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''. Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache. Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''. Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre. Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs. [[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]] : Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples. ==Les instructions de contrôle du cache== Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation. ===Les instructions de préchargement=== La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse. L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique. Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles. ===Les instructions d'invalidation et de ''flush''=== Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions. Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache. Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute. Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. À la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point. Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2. Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''. ===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées=== Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache. D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''. L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles. Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire. Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc. ==L'associativité des caches et leur adressage implicite== Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie. ===Les caches totalement associatifs=== Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches. [[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]] Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible. [[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]] Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur. [[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]] Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM. [[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]] Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs. [[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]] ===Les caches directement adressés=== Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion. [[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]] Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer. Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous. [[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]] Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache. [[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]] Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire. [[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]] L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches. [[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]] ===Les caches associatifs par voie=== Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie. [[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]] Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''. [[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]] Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs. [[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]] Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits. ==Les optimisations des caches associatifs par voie== Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations. ===Les caches pseudo-associatifs=== Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370. Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie. L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache. ===La prédiction de voie=== Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité. Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie. Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste. Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags. Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée : * soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ; * soit par l'adresse à accéder (là encore, quelques bits de poids faible) ; * soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ; * soit par autre chose. ===La mise en veille sélective des voies=== Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes. Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue. ===Les caches ''skew-associative''=== Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais). Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie. [[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]] ==L'adressage physique ou logique des caches== Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second. {| |[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]] |[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]] |} ===L'accès à un cache physiquement/virtuellement tagué=== La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds. Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides. Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs. [[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]] ===Les défauts des caches virtuellement tagués=== Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes. ====Les droits d'accès doivent être vérifiés lors d'un accès au cache==== Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs. Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable. ====Les adresses homonymes perturbent la gestion du cache==== Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser. Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur. Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur. Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors. L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU. ====Les adresses synonymes perturbent aussi la gestion du cache==== La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare ! Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes. ===Les caches virtuellement adressés, mais physiquement tagués=== Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies). L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse. Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache. L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''. [[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]] Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice. La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance. ==Le remplacement des lignes de cache== Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps. Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples. Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace. De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache. ===Le remplacement aléatoire=== Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat. ===FIFO : first in, first out=== Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire. [[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]] Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''. ===MRU : most recently used=== Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée. Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables. ===LFU : least frequently used=== Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur. [[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]] ===LRU : least recently used=== Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles. Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU. ===Les approximations du LRU=== Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants. L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies. Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement. Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture. {| |[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]] |[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]] |} ===LRU amélioré=== L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent. Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur. D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation. ==Les écritures dans le cache : gestion et optimisations== Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''. Avec un '''cache ''write-back''''', si une écriture a lieu dans une ligne de cache, elle ne concerne que ce cache. Elle n'est pas propagée dans les niveaux de cache inférieurs, ni en RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles. [[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]] Avec les '''caches ''Write-Through''''', toute écriture dans le cache est propagée dans les niveaux de cache inférieurs s'ils existent, puis en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont beaucoup plus simples à implémenter. Ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs. [[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]] ===Les caches ''Write-through''=== Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps. Cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache. Et le même problème survient quand les niveaux de cache inférieurs sont utilisés en écriture. L'écriture ne peut pas être propagée tant que le port d'écriture est occupé. Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures. C'est une mémoire FIFO dans laquelle on place temporairement les écritures, à savoir : les données à écrire en RAM, mais aussi l'adresse à laquelle écrire. Les écritures attendent dans le tampon d'écriture que les niveaux de cache inférieurs (ou la RAM) soient libres. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse sont fusionnées en une seule, dans le tampon d’écriture. Cela fait un peu de place dans le tampon d’écriture, ce qui lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''. L'usage d'un tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. Mais on peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO. ===Les caches ''Write-back''=== Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM. En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer''). [[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]] Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Le cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus. [[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]] Quelques processeurs des années 2010 avaient un cache L3 qui était un cache de victime. Les défauts à froid chargeaient la donnée associée dans le cache L2, plus rarement dans le cache L1 directement. En tout cas, les données ne passaient pas par le cache L3. Le seul moyen pour qu'une donnée atterrissent dans le cache L3 est qu'elles aient été évincées du cache L2. C'était une solution adaptée pour une époque où avoir un gros cache était primordial, mais où le budget en transistor était un peu serré. Faire du L3 un cache de victime économisait des transistors, qui pouvaient être utilisés pour faire grossir le cache. De nos jours, les budgets en transistors permettent de faire autrement. ===La configuration du fonctionnement du cache=== Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture. Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau. Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée. Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe. De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec. ===L’allocation sur écriture=== Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement. L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''. [[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]] Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs. [[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]] ===La cohérence des caches=== Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache. Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour. ==Les caches exclusifs et inclusifs== Au début du chapitre, nous avons abordé le fait qu'un processeur moderne intégre une hiérarchie de mémoire caches, avec un cache L1, un cache L2 et un cache L3. Il est intéressant de regarder comment sont réaprties les données dans ces différents niveaux de cache. Et cela nous amène à parler des caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple. {| |[[File:Exclusivecache.png|vignette|upright=0.5|Cache exclusif.]] |[[File:Inclusivecache.png|vignette|upright=0.5|Cache inclusif.]] |} Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps. [[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]] Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches. [[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]] Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème. Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds. Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques. ==Les caches eDRAM, sur la carte mère et autres== D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement. [[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]] Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. On parlait alors de '''''Cache on a stick''''' (COAST). On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. Les ''Cache on a stick'' ont été introduits sur les processeurs Motorola, et utilisés sur les IBM PC et les Macintosh de l'époque. Les ordinateurs Macintosh utilisaient de tels caches, pour la plupart des modèles. Les processeurs PA RISC de HP utilisaient aussi de tels caches déportés hors du processeur. Sur les modèles comme le 7100, le processeur n’intégrait aucun cache et devait se se débrouiller avec un unique cache situé hors du CPU. Derrière ces choix se cachait une volonté d'utiliser un maximum de transistors pour les unités de calcul et de contrôle. Le cache prenait de la place sur le ''die'', dont ils en déportaient un maximum hors du processeur. Les modèles ultérieurs, comme le PA 7300, avaient le budget en transistor pour intégrer des caches. Ils intégraient des caches L1 de données et d'instructions, mais le L2 était déporté hors du CPU. Les caches L1 étaient cependant assez larges, comparé à la moyenne de l'époque. Le choix d'HP était d'avoir un unique niveau de cache, optimisé de manière à pouvoir être à la fois rapide et de grande capacité. Pour ce qui est des PC, les premiers processeurs x86 faisaient pareil, notamment les processeurs Intel. Le 486, le Pentium et le Pentium 2 utilisaient des ''Cache on a stick''. L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner soit sans mémoire cache, soit avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin. Pour les CPU Intel, le cache était connecté sur le bus système, au même titre que la mémoire RAM et les entrées-sorties. Il faut dire que les processeurs de l'époque utilisaient un bus système et n'avaient pas de bus mémoire dédié. Mais en théorie, rien n’empêche de connecter le cache sur un bus mémoire dédié. Toujours est-il que les lectures et écritures étaient propagées à la fois dans le cache et la RAM. Les écritures se faisaient dans les deux, systématiquement dans la RAM, mais aussi dans le cache en cas de succès de cache. Les lectures étaient servies soit par le cache en cas de succès de cache, soit par la RAM en cas de défaut de cache. Si le cache répondait en premier, la transaction sur le bus se terminait précocement et l'accès en RAM était abandonné. [[File:Intel486 Иерархия памяти.png|centre|vignette|upright=2.5|Intel486 : le cache était connecté sur le bus système.]] À l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO. Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, que nous allons voir dans la section suivante. ==Le ''cache bypassing'' : contourner le cache== Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant. ===Accéder aux périphériques demande de contourner le cache=== Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire. La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes. Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si. Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable. Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci. ===Contourner le cache pour des raisons de performance=== Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard). Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache. L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non. ===Le cas particulier du processeur MIPS R8000=== Le processeur MIPS R8000 intégrait une forme de ''cache bypassing'', qui est, à ma connaissance, totalement inédite. Et pour la comprendre, faisons un léger rappel. Les processeurs qui supportent les nombres flottants disposent d'instructions de lecture/écriture spécialisées pour les nombres flottants, séparées des instructions LOAD et STORE classiques. Elles sont souvent appelées FLOAD et STORE. Les fait de séparer les instructions LOAD/STORE des instructions FLOAD/FSTORE permet d'implémenter des optimisations, ce qui est le cas ici. Sur le MIPS R8000, les instructions FLOAD et FSTORE contournaient le cache, à savoir que les flottants étaient lus/écrits sans passer par la mémoire cache. La raison à cela est que les applications de l'époque avaient une bonne localité spatiale et temporlle pour les instructions LOAD et STORE classiques, mais une très mauvaise localité spatiale et temporelle pour leurs équivalents flottant. Concrètement, les applications faisant beaucoup de calculs flottants profitaient peu de la présence de mémoire cache. Si les flottants passaient dans le cache, ils pollueraient le cache, à savoir qu'ils remplaceraient des données bien plus utiles. Les choses sont cependant compliquées par la hiérarchie de cache un peu bizarre de ce processeur. Le processeur MIPS R8000 avait deux caches L1, un pour les données et un pour les instructions. De plus, il pouvait utiliser un cache L2 externe, placé sur la carte mère. Les instructions LOAD et STORE ne contournaient pas les caches L1 et L2, ce qui fait que les nombres entiers et adresses pouvaient sans problème être copiés dans le cache L1 de données. Les instructions FLOAD et FSTORE ne passaient pas par le cache L1, mais atterrissaient dans le cache L2. En clair, le cache L1 de données était contourné, mais pas le cache L2. Toute lecture/écriture flottante se faisait dans le cache L2 externe, elle contournait automatiquement le cache L1. Comme toute forme de ''cache bypassing'', le MIPS R8000 devait faire face à des problèmes de cohérence des caches, qui apparaissaient des des situations bien spécifiques. Les problèmes survenaient quand des entiers et des flottants étaient mixés entre dans dans une même structure de donnée (voire quand un mot contenait indifféremment entier et flottant, grâce à l'usage d'unions). Une écriture flottante pouvait modifier un mot mémoire, sans que la copie dans le cache L1 soit modifiée. Idem avec une écriture entière, qui pouvait modifier un mot dans le cache L1, sans que le cache L2 soit au courant. Pour résoudre le problème, les écritures flottants étaient partiellement prises en compte par le cache L1. Le flottant écrit n'était pas écrit dans le cache L1, mais l'adresse de l'écriture était envoyée au cache L1, pour que celui-ci vérifie s'il détient une copie de cette adresse. Si c'est le cas, la ligne de cache correspondante était invalidée. Pour gérer ces invalidations, chaque ligne de cache était associée à un bit de validité, qui était mis à 1 lorsque la donnée était chargée depuis la mémoire RAM. Les écritures flottantes mettaient à 0 ce bit de validité, les écritures entières le remettaient à 1. ==Les caches splittés (''phased caches'')== Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Nous allons partir du principe que ce sont des caches ''direct-mapped'', mais l'optimisation peut être utilisée sur un cache associatif par voie. L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs. Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache. Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin. L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle. [[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]] Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres. Le processeur UltraSPARC III utilisait un cache splitté particulier, pour son cache L2. Son cache L1 était un cache normal, non(-splitté. Le cache était splitté : les ''tags'' étaient dans le processeur, alors que les données étaient dans un cache situé en-dehors du processeur, sur la carte mère. Faire ainsi permettait de déporter beaucoup de transistors en-dehors du processeur, tout en conservant des caches performants. Le fait d'intégrer les ''tags'' dans le processeur faisait que consulter les ''tags'' était très rapide, accéder aux données était plus lent. En contrepartie, la SRAM des données était bien plus grosse. ===Le contrôleur de cache 82385 pour les CPU Intel 386=== Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Voire que les deux sont séparés du processeur ! C'était le cas quand les mémoires caches ont été introduites sur les processeurs grand public, notamment sur les premiers processeurs Intel. La miniaturisation n'avait pas avancé au point où placer un cache dans le processeur était possible. Sur le processeur 386 d'Intel, le cache était un cache splitté, séparé du processeur. Concrètement, le processeur i386 était couplé à un contrôleur de cache Intel 82385 et une mémoire SRAM. Le 82385 contenait les ''tags'' et les bits de contrôle, la SRAM contenait les données, les lignes de cache. Un point important est que les lignes de cache faisaient seulement 32 bits/4 octets, pas plus ! On était loin des lignes de cache actuelles, faisant 64 octets/512 bits. Mais c'était beaucoup plus pratique, vu que le bus système faisait 32 bits de large, idem pour l'interface avec le processeur. Le schéma ci-dessous montre comment le cache s'intégrait avec le bus système. Pour le bus de commande, le cache servait d'intermédiaire : il recevait les commandes et les filtrait suivant les succès/défauts de cache. En cas de succès de cache, les commandes de lecture n'étaient pas envoyées à la mémoire RAM. Les adresses étaient transmises à la fois au cache et au bus système (avec un registre entre le bus système et le processeur). Le bus de donnée était lui connecté à la mémoire SRAM et au processeur, avec des MUX/DEMUX pour faire le choix de la source des lectures. [[File:Controleur de cache 82385 pour l'Intel 386.png|centre|vignette|upright=2.5|Contrôleur de cache 82385 pour l'Intel 386]] Le 82385 surveillait ce qui se passait sur le bus et répondait à la place de la RAM pour certaines lectures. C'était un intermédiaire assez passif, qui se contenait de répondre aux succès et défauts en lecture. Le cache était un cache ''write through'' un peu particulier. En cas de succès de cache pour une écriture, le cache met à jour sa ligne de cache et propage l'écriture en mémoire RAM. Par contre, si une écriture fait un défaut de cache, la donnée n'est pas écrite dans le cache. Le seul moyen pour copier une donnée dans le cache était un défaut pour une lecture. Le 82385 pouvait commander soit un cache ''direct mapped'', soit associatif à deux voies. Le choix entre les deux était le fait d'une entrée : la mettre à 0 indiquait un cache ''direct mapped'', la mettre à 1 forçait un cache à deux voies. La différence entre les deux est que le 82385 était relié à une mémoire SRAM avec un cache ''direct mapped'', deux SRAM pour deux voies. Pour avoir un cache associatif à deux voies, le 82385 devrait gérer deux signaux ''chip select'' pour activer chaque SRAM/voie suivant les besoins. Il avait précisément quatre signaux CS : deux par SRAM, un pour les lectures, un pour les écritures. Notons que les lignes de cache faisaient 32 bits, ce qui pouvait d'obtenir soit avec une SRAM 32 bits, soit avec deux SRAM 16 bits, soit avec 4 SRAM 8 bits. Le 82385 rajoutait 4 sorties, pour masquer chaque octet dans ces 32 bits, qui sont techniquement des signaux ''Output Enable'' pour 4 SRAM 8 bits. [[File:Interface entre le 82385 et la SRAM du cache.png|centre|vignette|upright=2|Interface entre le 82385 et la SRAM du cache. Beaucoup d'entrées et de sorties liées au bus d'adresse ne sont pas représentées.]] Il gérait aussi les accès mémoire non-cacheable, à savoir des accès mémoire qui ne doivent pas être pris en compte par le cache. Il considérait certains accès mémoire comme "à ne pas cacher". Notamment, les accès mémoire à une entrée-sortie ne sont pas cachés. Pour rappel, le processeur utilisait un espace d'adressage séparé pour les entrées-sorties, et utilisait donc un bit IO, qui était utilisé par le 82385 pour savoir si l'accès mémoire doit être caché ou non. Il en est de même pour les accès ayant lieu lors d'une interruption, qui ne passent pas par le cache. Mais au-delà de cette inhibition automatique du cache, le 82385 avait une entrée NCA (''Non Cacheable Access'') : le cache était "désactivé" quand cette entrée était à 1. C'est un peu une sorte de ''chip select'' pour le 82385, limitée aux accès mémoire. Cette entrée permettait de programmer des intervalles d'adresse auxquels ne pas répondre, en utilisant des circuits de décodage d'adresse adaptés. Il avait aussi une entrée X16, qui permettait d'identifier les accès soit à un composant 16 bits. De tels accès ne doivent pas être mis en cache, sans doute parce que cela ne collait pas avec la taille des lignes de cache (32 bits). Et cette entrée permettait d'inhiber ces accès 16 bits d'agir sur le cache, en utilisant le bit du bus de commande adéquat. Le 82385 pouvait être intégré dans un système à deux processeurs, voire plus. Pour cela, chaque processeur avait son propre 82385 et sa SRAM rien qu'à lui. Il n'y avait pas de cache partagé entre les deux processeurs. Par contre, les deux caches étaient reliés au même bus système. Pour qu'ils ne se marchent pas sur les pieds, il y avait des circuits d'arbitrage pour gérer l'accès au bus. Un des deux 82385 était mis en mode maitre, l'autre était en mode esclave. Le 82385 maitre pouvait prendre le contrôle du bus, le 82385 esclave devait demander l'autorisation au premier pour accéder au bus système. Le 82385 gérait une forme limitée de cohérence des caches par invalidation. Dès que le 82385 détectait une prise de contrôle du bus par autre chose que le processeur, il surveillait les adresses transmises sur le bus. En cas de succès de cache, la ligne de cache associée était invalidée. Au-delà de ça, le 82385 avait une entrée FLUSH, qui ordonnait une invalidation totale du cache. Si cette entrée est mise à 1, toutes les lignes de cache sont invalidées. Les ''tags'' sont marqués comme invalides, mais les lignes de cache elles-mêmes ne sont pas touchées. ===L'exemple des processeurs Intel de microarchitecture ''Broadwell''=== Un autre exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO ! La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes. Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. À chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données. Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags. ===Les caches RAM-configurables=== Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable. [[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]] ===La compression de cache=== Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs. Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache). Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ? [[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]] Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée. Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux : * [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec]. ==Les caches adressés par somme et hashés== Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme. Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement. Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse. [[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]] Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante. [[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]] Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit : : <math>A + B = K</math> Ce qui est équivalent à faire le test suivant : : <math>A + B - K = 0</math> En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a : : <math>A + B + \overline{K} + 1 = 0</math> En réorganisant les termes, on a : : <math>A + B + \overline{K} = - 1</math> Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a: : <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>. Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux. : <math>S + (R << 1) = 111 \cdots 111111</math> [[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]] Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors : : <math>S \oplus (R << 1) = 111 \cdots 111111</math> La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester. Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances. [[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]] En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable. ==Les caches à accès uniforme et non-uniforme== Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres. Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme. [[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]] Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme. Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe. Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant. Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA''). Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique. ==La tolérance aux erreurs des caches== Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens. ===Les mémoires caches ECC et à bit de parité=== Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé. Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits. La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''. Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC. ===L'usage du ''memory scrubbing'' sur les caches=== La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance. Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire. Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable. ===Le cas particulier des caches ''Write-Through''=== Les caches ''Write-Through'' et ''Write-Back'' ne sont pas égaux du point de vue de l'ECC. En effet, les caches ''Write-Back'' ont un désavantage : quand une donnée est modifiée dans le cache, seul le cache a une copie de la donnée valide. Ils ont donc intérêt à protéger cette donnée contre les erreurs, ce qui implique l'usage de l'ECC au minimum. A l'opposé, les caches ''Write-Through'' n'ont pas ce problème : ils garantissent que toute donnée dans le cache est aussi recopiée ailleurs, que ce soit en mémoire RAM, dans le niveau de cache inférieur, ou dans un ''write buffer''/tampon d'écriture. Ils peuvent donc se contenter d'utiliser un bit de parité par mot, voire par ligne de cache. Ce qui entraine des économies substantielles de circuits. En général, les processeurs préfèrent utiliser des caches ''Write-Back'' pour tous les niveaux, L1, L2 et L3, voire L4. Mais quelques rares processeurs préféraient utiliser des ''Write-Through'' pour le niveau L1. Je précise bien : uniquement pour le niveau L1, pas pour les niveaux L2 et L3. Les niveaux L2 et L3 utilisaient des caches ''Write-Back'' protégés avec de l'ECC, seuls les caches L1 se débrouillaient sans. Un exemple est le Pentium 4 d'Intel, qui se débrouillait avec un L1 ''Write-Through'', sans tampon d'écriture. Les performances en écritures étaient de fait très mauvaises, du fait de l'absence de tampon d'ériture. Les processeurs AMD de microarchitecture Bulldozer avaient eu aussi un cache L1 ''Write-Through'', mais combinés avec un tampon d'écriture, pour conserver des performances acceptables. Le tampon d'écriture en question était un cache de 4 kibioctets, à 4 voies. Les performances du cache en écriture étaient cependant inférieures à celles des processeurs concurrents, qui utailisaient des caches ''Write-Back'' ==Un exemple de cache : le cache d'instruction== [[File:Separate unified.png|thumb|Cache d'instruction séparé du cache de données.]] La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions. Les deux caches sont reliés au processeur par des bus séparés, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une '''architecture Harvard modifiée'''. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM. Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Le cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur. [[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]] ===Pourquoi scinder le cache L1 en cache d'instruction et de données=== L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache L1 multiport unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs avaient un cache L1 unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes. Le compromis à faire est celui entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. En termes de temps d'accès, la meilleure solution est celle des deux caches simple port. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. La différence est surtout notable pour les gros caches, moins pour les petits caches. Il faut aussi tenir compte de la capacité effective. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 KB et un cache de données de 32 KB, impossible d'allouer 40 KB aux données et 20 aux instructions. Alors qu'avec un cache L1 unique de 64 KB, on pourrait le faire sans soucis. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre. Tout cela explique pourquoi le cache L1 est le seul à être ainsi scindé en deux, avec une séparation entre instructions et données : les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Pour les caches L1, le temps d'accès est plus important que la capacité, ce qui favorise les caches séparés. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que la capacité effective et l'économie en circuits sont significatives. ===La connexion des caches L1 avec le cache L2=== Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément. [[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]] Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé. [[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]] Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre ! ===Les spécificités du cache d'instruction : lecture seule, bloquant, etc=== Les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans. Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés. Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour. Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale. Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. À l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches. ===L'impact du cache d'instruction sur les performances=== Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions. : La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale. La conséquence est qu'il arrive que certains CPU aient un cache L1 d'instruction plus gros que celui pour les données. On parle alors de '''cache L1 asymétriques'''. Un exemple est celui des processeurs AMD de microarchitecture Zen, dont le cache d'instruction était deux fois plus gros que le cache de données. Leur cache d'instruction faisait 64 kibioctets, contre seulement 32 pour le cache de données. D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Compléments sur les mémoires de masse | prevText=Compléments sur les mémoires de masse | next=Le préchargement | nextText=Le préchargement }} </noinclude> 13gn8f33xdj30ds8jjeykco2k5wvhg1 773893 773892 2026-10-01T17:33:07Z Mewtow 31375 773893 wikitext text/x-wiki La '''mémoire cache''' est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Sans elle, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire RAM est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instructions (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente. Le cache est fabriqué avec de la mémoire SRAM, avec quelques exceptions qui utilisent de l'eDRAM. Les tout premiers processeurs utilisaient un cache externe, séparé du processeur, qui était installé sur un connecteur dédié. Mais de nos jours, le cache est intégré dans le processeur, directement. La loi de Moore a permis d'utiliser des transistors pour le cache, en plus des cœurs du processeur. Et le gain de cette intégration est assez clair : le cache est plus rapide quand il est dans le processeur que dehors. Mais même l'intégration du cache dans le processeur ne suffit plus de nos jours. Les processeurs sont devenus tellement rapides qu'ils ne peuvent plus se contenter d'un seul cache ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle. Et qui dit même problème, dit même solution ! Les processeurs modernes utilisent une hiérarchie de mémoires caches, allant d'un cache très petit mais très rapide, à un cache plus lent mais de grande capacité. Les processeurs assez anciens n'utilisaient pas de cache L3, ils avaient juste un cache L1 petit et rapide, et un cache L2 gros et lent. [[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]] Depuis les années 2010, les processeurs utilisent une hiérarchie à trois niveaux de cache. Les caches en question sont appelés le '''cache L1''' pour le plus petit et le plus rapide, le '''cache L2''' pour le cache intermédiaire, et le '''cache L3''' pour le cache gros et lent. Il arrive même que certains processeurs aient un '''cache L4''', très gros mais aussi très lent. Le cache L3/L4 a parfois des caractéristiques totalement différentes des autres caches. Par exemple, sur les processeurs multicœurs, le cache L3 n'est techniquement pas dans le cœur, mais fait partie du contrôleur mémoire ou de l'interface mémoire. Il fonctionne à une fréquence différente du processeur, n'a pas la même tension d'alimentation, etc. [[File:Cache Hierarchy.png|centre|vignette|upright=1.5|Hiérarchie de caches]] La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un '''cache L1 de données''' pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions. Le cache L2 mémorise indifféremment données et instructions. La séparation entre cache L1 de données et d’instruction est apparue en même temps que la scission entre cache L1 et L2. [[File:Cache d'instructions.png|centre|vignette|upright=2|Cache d'instructions.]] Le nombre de niveaux de cache varient d'un processeur à l'autre. Les tout premiers processeurs faisaient avec un cache unique, sans hiérarchie de cache. Mais par la suite, la scission entre L1 et L2 est apparue. Puis un cache L3 a été ajouté, avec parfois un cache L4. Pour rester général, la hiérarchie de caches est composée de plusieurs '''niveaux de cache''', qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides. Le cache le plus proche de la mémoire est appelé le '''cache de dernier niveau''', ''Last Level Cache'' en anglais. Concrètement, c'est le cache L2 sur les anciens processeurs, le cache L3 ou L4 sur les processeurs modernes. Petit point terminologie avant de poursuivre : j'utiliserais parfois l'expression "niveaux de cache inférieurs". L'expression se fait quand on parle d'un niveau de cache bien précis, souvent le niveau L1. Quand on parle des "niveaux de cache inférieurs au L1", cela veut dire le cache L2 et L3, voire ceux en-dessous s'ils existent. Idem pour les "niveaux de cache inférieurs au L2", qui désigne le cache L3. La mémoire RAM est souvent implicitement comptée dans cette expression, même si elle n'est pas un cache à proprement parler. ==L'accès au cache== Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur. Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard. [[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]] La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là. [[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]] ===Les succès et défauts de caches=== Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM. Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général. La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas). Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours. ===Le fonctionnement du cache, vu du processeur=== Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets. Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés. Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés. [[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]] ===LA situation avec une hiérarchie de caches=== Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas, on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire. [[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]] ==La performance des mémoires caches== L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache. ===Le taux de succès/défaut=== Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à : : <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math> Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à : : <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math> Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas. Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes. Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres. Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena. {{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}} ===La latence moyenne d'un cache=== Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>. En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors : : <math>T = T_c + \text{Taux de défaut} \times T_m</math> On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs. Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants). ===L'impact de la taille du cache sur le taux de défaut et la latence=== Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches. Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge. Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi : : <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté. Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache. L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment : : <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations. Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations. Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide. La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long. ==Les lignes de cache et leurs tags== Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes. ===Les lignes de cache=== Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM. En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille. Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples. ===L'alignement des lignes de cache=== Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne. Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache. L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants. L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Si une données, comme un entier ou un flottant, est à cheval sur deux lignes de cache, elle sera chargée en deux fois, en deux lectures. idem pour les écritures. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique. ===Le tag d'une ligne de cache=== Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''. Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante. [[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]] Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée. [[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]] ===Le contenu d'une ligne de cache=== Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré. [[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]] Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''. Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache. Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''. Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre. Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs. [[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]] : Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples. ==Les instructions de contrôle du cache== Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation. ===Les instructions de préchargement=== La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse. L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique. Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles. ===Les instructions d'invalidation et de ''flush''=== Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions. Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache. Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute. Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. À la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point. Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2. Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''. ===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées=== Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache. D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''. L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles. Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire. Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc. ==L'associativité des caches et leur adressage implicite== Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie. ===Les caches totalement associatifs=== Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches. [[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]] Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible. [[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]] Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur. [[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]] Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM. [[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]] Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs. [[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]] ===Les caches directement adressés=== Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion. [[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]] Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer. Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous. [[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]] Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache. [[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]] Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire. [[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]] L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches. [[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]] ===Les caches associatifs par voie=== Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie. [[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]] Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''. [[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]] Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs. [[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]] Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits. ==Les optimisations des caches associatifs par voie== Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations. ===Les caches pseudo-associatifs=== Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370. Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie. L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache. ===La prédiction de voie=== Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité. Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie. Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste. Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags. Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée : * soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ; * soit par l'adresse à accéder (là encore, quelques bits de poids faible) ; * soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ; * soit par autre chose. ===La mise en veille sélective des voies=== Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes. Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue. ===Les caches ''skew-associative''=== Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais). Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie. [[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]] ==L'adressage physique ou logique des caches== Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second. {| |[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]] |[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]] |} ===L'accès à un cache physiquement/virtuellement tagué=== La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds. Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides. Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs. [[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]] ===Les défauts des caches virtuellement tagués=== Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes. ====Les droits d'accès doivent être vérifiés lors d'un accès au cache==== Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs. Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable. ====Les adresses homonymes perturbent la gestion du cache==== Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser. Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur. Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur. Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors. L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU. ====Les adresses synonymes perturbent aussi la gestion du cache==== La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare ! Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes. ===Les caches virtuellement adressés, mais physiquement tagués=== Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies). L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse. Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache. L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''. [[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]] Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice. La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance. ==Le remplacement des lignes de cache== Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps. Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples. Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace. De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache. ===Le remplacement aléatoire=== Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat. ===FIFO : first in, first out=== Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire. [[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]] Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''. ===MRU : most recently used=== Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée. Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables. ===LFU : least frequently used=== Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur. [[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]] ===LRU : least recently used=== Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles. Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU. ===Les approximations du LRU=== Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants. L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies. Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement. Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture. {| |[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]] |[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]] |} ===LRU amélioré=== L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent. Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur. D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation. ==Les écritures dans le cache : gestion et optimisations== Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''. Avec un '''cache ''write-back''''', si une écriture a lieu dans une ligne de cache, elle ne concerne que ce cache. Elle n'est pas propagée dans les niveaux de cache inférieurs, ni en RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles. [[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]] Avec les '''caches ''Write-Through''''', toute écriture dans le cache est propagée dans les niveaux de cache inférieurs s'ils existent, puis en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont beaucoup plus simples à implémenter. Ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs. [[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]] ===Les caches ''Write-through''=== Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps. Cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache. Et le même problème survient quand les niveaux de cache inférieurs sont utilisés en écriture. L'écriture ne peut pas être propagée tant que le port d'écriture est occupé. Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures. C'est une mémoire FIFO dans laquelle on place temporairement les écritures, à savoir : les données à écrire en RAM, mais aussi l'adresse à laquelle écrire. Les écritures attendent dans le tampon d'écriture que les niveaux de cache inférieurs (ou la RAM) soient libres. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse sont fusionnées en une seule, dans le tampon d’écriture. Cela fait un peu de place dans le tampon d’écriture, ce qui lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''. L'usage d'un tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. Mais on peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO. ===Les caches ''Write-back''=== Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM. En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer''). [[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]] Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Le cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus. [[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]] Quelques processeurs des années 2010 avaient un cache L3 qui était un cache de victime. Les défauts à froid chargeaient la donnée associée dans le cache L2, plus rarement dans le cache L1 directement. En tout cas, les données ne passaient pas par le cache L3. Le seul moyen pour qu'une donnée atterrissent dans le cache L3 est qu'elles aient été évincées du cache L2. C'était une solution adaptée pour une époque où avoir un gros cache était primordial, mais où le budget en transistor était un peu serré. Faire du L3 un cache de victime économisait des transistors, qui pouvaient être utilisés pour faire grossir le cache. De nos jours, les budgets en transistors permettent de faire autrement. ===La configuration du fonctionnement du cache=== Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture. Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau. Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée. Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe. De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec. ===L’allocation sur écriture=== Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement. L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''. [[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]] Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs. [[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]] ===La cohérence des caches=== Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache. Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour. ==Les caches exclusifs et inclusifs== Au début du chapitre, nous avons abordé le fait qu'un processeur moderne intégre une hiérarchie de mémoire caches, avec un cache L1, un cache L2 et un cache L3. Il est intéressant de regarder comment sont réaprties les données dans ces différents niveaux de cache. Et cela nous amène à parler des caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple. {| |[[File:Exclusivecache.png|vignette|upright=0.5|Cache exclusif.]] |[[File:Inclusivecache.png|vignette|upright=0.5|Cache inclusif.]] |} Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps. [[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]] Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches. [[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]] Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème. Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds. Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques. ==Les caches eDRAM, sur la carte mère et autres== D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement. [[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]] Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. On parlait alors de '''''Cache on a stick''''' (COAST). On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM. Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. Les ''Cache on a stick'' ont été introduits sur les processeurs Motorola, et utilisés sur les IBM PC et les Macintosh de l'époque. Les ordinateurs Macintosh utilisaient de tels caches, pour la plupart des modèles. Les processeurs PA RISC de HP utilisaient aussi de tels caches déportés hors du processeur. Sur les modèles comme le 7100, le processeur n’intégrait aucun cache et devait se se débrouiller avec un unique cache situé hors du CPU. Derrière ces choix se cachait une volonté d'utiliser un maximum de transistors pour les unités de calcul et de contrôle. Le cache prenait de la place sur le ''die'', dont ils en déportaient un maximum hors du processeur. Les modèles ultérieurs, comme le PA 7300, avaient le budget en transistor pour intégrer des caches. Ils intégraient des caches L1 de données et d'instructions, mais le L2 était déporté hors du CPU. Les caches L1 étaient cependant assez larges, comparé à la moyenne de l'époque. Le choix d'HP était d'avoir un unique niveau de cache, optimisé de manière à pouvoir être à la fois rapide et de grande capacité. Pour ce qui est des PC, les premiers processeurs x86 faisaient pareil, notamment les processeurs Intel. Le 486, le Pentium et le Pentium 2 utilisaient des ''Cache on a stick''. L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner soit sans mémoire cache, soit avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin. Pour les CPU Intel, le cache était connecté sur le bus système, au même titre que la mémoire RAM et les entrées-sorties. Il faut dire que les processeurs de l'époque utilisaient un bus système et n'avaient pas de bus mémoire dédié. Mais en théorie, rien n’empêche de connecter le cache sur un bus mémoire dédié. Toujours est-il que les lectures et écritures étaient propagées à la fois dans le cache et la RAM. Les écritures se faisaient dans les deux, systématiquement dans la RAM, mais aussi dans le cache en cas de succès de cache. Les lectures étaient servies soit par le cache en cas de succès de cache, soit par la RAM en cas de défaut de cache. Si le cache répondait en premier, la transaction sur le bus se terminait précocement et l'accès en RAM était abandonné. [[File:Intel486 Иерархия памяти.png|centre|vignette|upright=2.5|Intel486 : le cache était connecté sur le bus système.]] À l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO. Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, que nous allons voir dans la section suivante. ==Le ''cache bypassing'' : contourner le cache== Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant. ===Accéder aux périphériques demande de contourner le cache=== Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire. La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes. Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si. Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable. Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci. ===Contourner le cache pour des raisons de performance=== Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard). Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache. L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non. ===Le cas particulier du processeur MIPS R8000=== Le processeur MIPS R8000 intégrait une forme de ''cache bypassing'', qui est, à ma connaissance, totalement inédite. Et pour la comprendre, faisons un léger rappel. Les processeurs qui supportent les nombres flottants disposent d'instructions de lecture/écriture spécialisées pour les nombres flottants, séparées des instructions LOAD et STORE classiques. Elles sont souvent appelées FLOAD et STORE. Les fait de séparer les instructions LOAD/STORE des instructions FLOAD/FSTORE permet d'implémenter des optimisations, ce qui est le cas ici. Sur le MIPS R8000, les instructions FLOAD et FSTORE contournaient le cache, à savoir que les flottants étaient lus/écrits sans passer par la mémoire cache. La raison à cela est que les applications de l'époque avaient une bonne localité spatiale et temporlle pour les instructions LOAD et STORE classiques, mais une très mauvaise localité spatiale et temporelle pour leurs équivalents flottant. Concrètement, les applications faisant beaucoup de calculs flottants profitaient peu de la présence de mémoire cache. Si les flottants passaient dans le cache, ils pollueraient le cache, à savoir qu'ils remplaceraient des données bien plus utiles. Les choses sont cependant compliquées par la hiérarchie de cache un peu bizarre de ce processeur. Le processeur MIPS R8000 avait deux caches L1, un pour les données et un pour les instructions. De plus, il pouvait utiliser un cache L2 externe, placé sur la carte mère. Les instructions LOAD et STORE ne contournaient pas les caches L1 et L2, ce qui fait que les nombres entiers et adresses pouvaient sans problème être copiés dans le cache L1 de données. Les instructions FLOAD et FSTORE ne passaient pas par le cache L1, mais atterrissaient dans le cache L2. En clair, le cache L1 de données était contourné, mais pas le cache L2. Toute lecture/écriture flottante se faisait dans le cache L2 externe, elle contournait automatiquement le cache L1. Comme toute forme de ''cache bypassing'', le MIPS R8000 devait faire face à des problèmes de cohérence des caches, qui apparaissaient des des situations bien spécifiques. Les problèmes survenaient quand des entiers et des flottants étaient mixés entre dans dans une même structure de donnée (voire quand un mot contenait indifféremment entier et flottant, grâce à l'usage d'unions). Une écriture flottante pouvait modifier un mot mémoire, sans que la copie dans le cache L1 soit modifiée. Idem avec une écriture entière, qui pouvait modifier un mot dans le cache L1, sans que le cache L2 soit au courant. Pour résoudre le problème, les écritures flottants étaient partiellement prises en compte par le cache L1. Le flottant écrit n'était pas écrit dans le cache L1, mais l'adresse de l'écriture était envoyée au cache L1, pour que celui-ci vérifie s'il détient une copie de cette adresse. Si c'est le cas, la ligne de cache correspondante était invalidée. Pour gérer ces invalidations, chaque ligne de cache était associée à un bit de validité, qui était mis à 1 lorsque la donnée était chargée depuis la mémoire RAM. Les écritures flottantes mettaient à 0 ce bit de validité, les écritures entières le remettaient à 1. ==Les caches splittés (''phased caches'')== Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Nous allons partir du principe que ce sont des caches ''direct-mapped'', mais l'optimisation peut être utilisée sur un cache associatif par voie. L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs. Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache. Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin. L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle. [[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]] Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres. Le processeur UltraSPARC III utilisait un cache splitté particulier, pour son cache L2. Son cache L1 était un cache normal, non(-splitté. Le cache était splitté : les ''tags'' étaient dans le processeur, alors que les données étaient dans un cache situé en-dehors du processeur, sur la carte mère. Faire ainsi permettait de déporter beaucoup de transistors en-dehors du processeur, tout en conservant des caches performants. Le fait d'intégrer les ''tags'' dans le processeur faisait que consulter les ''tags'' était très rapide, accéder aux données était plus lent. En contrepartie, la SRAM des données était bien plus grosse. ===Le contrôleur de cache 82385 pour les CPU Intel 386=== Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Voire que les deux sont séparés du processeur ! C'était le cas quand les mémoires caches ont été introduites sur les processeurs grand public, notamment sur les premiers processeurs Intel. La miniaturisation n'avait pas avancé au point où placer un cache dans le processeur était possible. Sur le processeur 386 d'Intel, le cache était un cache splitté, séparé du processeur. Concrètement, le processeur i386 était couplé à un contrôleur de cache Intel 82385 et une mémoire SRAM. Le 82385 contenait les ''tags'' et les bits de contrôle, la SRAM contenait les données, les lignes de cache. Un point important est que les lignes de cache faisaient seulement 32 bits/4 octets, pas plus ! On était loin des lignes de cache actuelles, faisant 64 octets/512 bits. Mais c'était beaucoup plus pratique, vu que le bus système faisait 32 bits de large, idem pour l'interface avec le processeur. Le schéma ci-dessous montre comment le cache s'intégrait avec le bus système. Pour le bus de commande, le cache servait d'intermédiaire : il recevait les commandes et les filtrait suivant les succès/défauts de cache. En cas de succès de cache, les commandes de lecture n'étaient pas envoyées à la mémoire RAM. Les adresses étaient transmises à la fois au cache et au bus système (avec un registre entre le bus système et le processeur). Le bus de donnée était lui connecté à la mémoire SRAM et au processeur, avec des MUX/DEMUX pour faire le choix de la source des lectures. [[File:Controleur de cache 82385 pour l'Intel 386.png|centre|vignette|upright=2.5|Contrôleur de cache 82385 pour l'Intel 386]] Le 82385 surveillait ce qui se passait sur le bus et répondait à la place de la RAM pour certaines lectures. C'était un intermédiaire assez passif, qui se contenait de répondre aux succès et défauts en lecture. Le cache était un cache ''write through'' un peu particulier. En cas de succès de cache pour une écriture, le cache met à jour sa ligne de cache et propage l'écriture en mémoire RAM. Par contre, si une écriture fait un défaut de cache, la donnée n'est pas écrite dans le cache. Le seul moyen pour copier une donnée dans le cache était un défaut pour une lecture. Le 82385 pouvait commander soit un cache ''direct mapped'', soit associatif à deux voies. Le choix entre les deux était le fait d'une entrée : la mettre à 0 indiquait un cache ''direct mapped'', la mettre à 1 forçait un cache à deux voies. La différence entre les deux est que le 82385 était relié à une mémoire SRAM avec un cache ''direct mapped'', deux SRAM pour deux voies. Pour avoir un cache associatif à deux voies, le 82385 devrait gérer deux signaux ''chip select'' pour activer chaque SRAM/voie suivant les besoins. Il avait précisément quatre signaux CS : deux par SRAM, un pour les lectures, un pour les écritures. Notons que les lignes de cache faisaient 32 bits, ce qui pouvait d'obtenir soit avec une SRAM 32 bits, soit avec deux SRAM 16 bits, soit avec 4 SRAM 8 bits. Le 82385 rajoutait 4 sorties, pour masquer chaque octet dans ces 32 bits, qui sont techniquement des signaux ''Output Enable'' pour 4 SRAM 8 bits. [[File:Interface entre le 82385 et la SRAM du cache.png|centre|vignette|upright=2|Interface entre le 82385 et la SRAM du cache. Beaucoup d'entrées et de sorties liées au bus d'adresse ne sont pas représentées.]] Il gérait aussi les accès mémoire non-cacheable, à savoir des accès mémoire qui ne doivent pas être pris en compte par le cache. Il considérait certains accès mémoire comme "à ne pas cacher". Notamment, les accès mémoire à une entrée-sortie ne sont pas cachés. Pour rappel, le processeur utilisait un espace d'adressage séparé pour les entrées-sorties, et utilisait donc un bit IO, qui était utilisé par le 82385 pour savoir si l'accès mémoire doit être caché ou non. Il en est de même pour les accès ayant lieu lors d'une interruption, qui ne passent pas par le cache. Mais au-delà de cette inhibition automatique du cache, le 82385 avait une entrée NCA (''Non Cacheable Access'') : le cache était "désactivé" quand cette entrée était à 1. C'est un peu une sorte de ''chip select'' pour le 82385, limitée aux accès mémoire. Cette entrée permettait de programmer des intervalles d'adresse auxquels ne pas répondre, en utilisant des circuits de décodage d'adresse adaptés. Il avait aussi une entrée X16, qui permettait d'identifier les accès soit à un composant 16 bits. De tels accès ne doivent pas être mis en cache, sans doute parce que cela ne collait pas avec la taille des lignes de cache (32 bits). Et cette entrée permettait d'inhiber ces accès 16 bits d'agir sur le cache, en utilisant le bit du bus de commande adéquat. Le 82385 pouvait être intégré dans un système à deux processeurs, voire plus. Pour cela, chaque processeur avait son propre 82385 et sa SRAM rien qu'à lui. Il n'y avait pas de cache partagé entre les deux processeurs. Par contre, les deux caches étaient reliés au même bus système. Pour qu'ils ne se marchent pas sur les pieds, il y avait des circuits d'arbitrage pour gérer l'accès au bus. Un des deux 82385 était mis en mode maitre, l'autre était en mode esclave. Le 82385 maitre pouvait prendre le contrôle du bus, le 82385 esclave devait demander l'autorisation au premier pour accéder au bus système. Le 82385 gérait une forme limitée de cohérence des caches par invalidation. Dès que le 82385 détectait une prise de contrôle du bus par autre chose que le processeur, il surveillait les adresses transmises sur le bus. En cas de succès de cache, la ligne de cache associée était invalidée. Au-delà de ça, le 82385 avait une entrée FLUSH, qui ordonnait une invalidation totale du cache. Si cette entrée est mise à 1, toutes les lignes de cache sont invalidées. Les ''tags'' sont marqués comme invalides, mais les lignes de cache elles-mêmes ne sont pas touchées. ===L'exemple des processeurs Intel de microarchitecture ''Broadwell''=== Un autre exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO ! La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes. Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. À chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données. Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags. ===Les caches RAM-configurables=== Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable. [[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]] ===La compression de cache=== Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs. Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache). Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ? [[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]] Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée. Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux : * [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec]. ==Les caches adressés par somme et hashés== Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme. Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement. Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse. [[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]] Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante. [[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]] Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit : : <math>A + B = K</math> Ce qui est équivalent à faire le test suivant : : <math>A + B - K = 0</math> En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a : : <math>A + B + \overline{K} + 1 = 0</math> En réorganisant les termes, on a : : <math>A + B + \overline{K} = - 1</math> Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a: : <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>. Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux. : <math>S + (R << 1) = 111 \cdots 111111</math> [[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]] Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors : : <math>S \oplus (R << 1) = 111 \cdots 111111</math> La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester. Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances. [[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]] En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable. ==Les caches à accès uniforme et non-uniforme== Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres. Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme. [[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]] Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme. Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe. Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant. Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA''). Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique. ==La tolérance aux erreurs des caches== Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens. ===Les mémoires caches ECC et à bit de parité=== Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé. Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits. La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''. Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC. ===L'usage du ''memory scrubbing'' sur les caches=== La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance. Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire. Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable. ===Le cas particulier des caches ''Write-Through''=== Les caches ''Write-Through'' et ''Write-Back'' ne sont pas égaux du point de vue de l'ECC. En effet, les caches ''Write-Back'' ont un désavantage : quand une donnée est modifiée dans le cache, seul le cache a une copie de la donnée valide. Ils ont donc intérêt à protéger cette donnée contre les erreurs, ce qui implique l'usage de l'ECC au minimum. A l'opposé, les caches ''Write-Through'' n'ont pas ce problème : ils garantissent que toute donnée dans le cache est aussi recopiée ailleurs, que ce soit en mémoire RAM, dans le niveau de cache inférieur, ou dans un ''write buffer''/tampon d'écriture. Ils peuvent donc se contenter d'utiliser un bit de parité par mot, voire par ligne de cache. Ce qui entraine des économies substantielles de circuits. En général, les processeurs préfèrent utiliser des caches ''Write-Back'' pour tous les niveaux, L1, L2 et L3, voire L4. Mais quelques rares processeurs préféraient utiliser des ''Write-Through'' pour le niveau L1. Je précise bien : uniquement pour le niveau L1, pas pour les niveaux L2 et L3. Les niveaux L2 et L3 utilisaient des caches ''Write-Back'' protégés avec de l'ECC, seuls les caches L1 se débrouillaient sans. Un exemple est le Pentium 4 d'Intel, qui se débrouillait avec un L1 ''Write-Through'', sans tampon d'écriture. Les performances en écritures étaient de fait très mauvaises, du fait de l'absence de tampon d'ériture. Les processeurs AMD de microarchitecture Bulldozer avaient eu aussi un cache L1 ''Write-Through'', mais combinés avec un tampon d'écriture, pour conserver des performances acceptables. Le tampon d'écriture en question était un cache de 4 kibioctets, à 4 voies. Les performances du cache en écriture étaient cependant inférieures à celles des processeurs concurrents, qui utailisaient des caches ''Write-Back'' ==Un exemple de cache : le cache d'instruction== [[File:Separate unified.png|thumb|Cache d'instruction séparé du cache de données.]] La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions. Les deux caches sont reliés au processeur par des bus séparés, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une '''architecture Harvard modifiée'''. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM. Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre. Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Le cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur. [[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]] ===Pourquoi scinder le cache L1 en cache d'instruction et de données=== L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache L1 multiport unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs avaient un cache L1 unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes. Le compromis à faire est celui entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. En termes de temps d'accès, la meilleure solution est celle des deux caches simple port. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. La différence est surtout notable pour les gros caches, moins pour les petits caches. Il faut aussi tenir compte de la capacité effective. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 KB et un cache de données de 32 KB, impossible d'allouer 40 KB aux données et 20 aux instructions. Alors qu'avec un cache L1 unique de 64 KB, on pourrait le faire sans soucis. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre. Tout cela explique pourquoi le cache L1 est le seul à être ainsi scindé en deux, avec une séparation entre instructions et données : les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Pour les caches L1, le temps d'accès est plus important que la capacité, ce qui favorise les caches séparés. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que la capacité effective et l'économie en circuits sont significatives. ===La connexion des caches L1 avec le cache L2=== Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément. [[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]] Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé. [[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]] Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre ! ===Les spécificités du cache d'instruction : lecture seule, bloquant, etc=== Les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans. Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés. Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour. Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale. Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. À l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches. ===L'impact du cache d'instruction sur les performances=== Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions. : La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale. La conséquence est qu'il arrive que certains CPU aient un cache L1 d'instruction plus gros que celui pour les données. On parle alors de '''cache L1 asymétriques'''. Un exemple est celui des processeurs AMD de microarchitecture Zen, dont le cache d'instruction était deux fois plus gros que le cache de données. Leur cache d'instruction faisait 64 kibioctets, contre seulement 32 pour le cache de données. D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Compléments sur les mémoires de masse | prevText=Compléments sur les mémoires de masse | next=Le préchargement | nextText=Le préchargement }} </noinclude> t4bgchcus101zu8iwn91a1msjgi1y8e Fonctionnement d'un ordinateur/Les architectures à parallélisme de données 0 65960 773846 773373 2026-10-01T12:19:16Z Mewtow 31375 Correction terme 773846 wikitext text/x-wiki Nous allons maintenant aborder le parallélisme de données, qui consiste à traiter des données différentes en parallèle. De nombreuses situations s'y prêtent relativement bien : traitement d'image, manipulation de sons, vidéo, rendu 3d, etc. Mais pour exploiter ce parallélisme, il a fallu concevoir des processeurs adaptés. Les architectures les plus simples exécutent une instruction sur plusieurs données en parallèle, à l'intérieur d'un processeur. Ces architectures exploitent le parallélisme de données au niveau de l'unité de calcul, celle-ci pouvant exécuter un même calcul sur des données différentes en parallèle. Si on omet quelques exceptions, on peut classer ces architectures dans plusieurs catégories principales : les processeurs à instructions SIMD, les processeurs à instructions SIMD à prédicat, les processeurs SIMT, les processeurs vectoriels. Si les processeurs vectoriels sont assez rares de nos jours, tous les processeurs récents incorporent des instructions SIMD. Quant au SIMT, il est utilisé sur les cartes graphiques modernes. ==Les instructions SIMD : les points communs entre tous les processeurs SIMD== Les processeurs modernes fournissent presque tous des '''instructions SIMD''', qui sont capables de traiter plusieurs éléments en parallèle. Elles travaillent sur des nombres entiers ou flottants regroupés dans ce qu'on appelle un ''vecteur'', qui sont eux-mêmes stockés dans des registres spécialisés. En général, tous les vecteurs ont une taille fixe, peu importe leur contenu. Cela implique que suivant la taille des données à manipuler, on pourra en placer plus ou moins dans un vecteur. Par exemple, un vecteur de 128 bits pourra contenir 4 entiers de 32 bits, 4 flottants 32 bits, ou 8 entiers de 16 bits. [[File:Vector register.png|centre|vignette|upright=2|Contenu d'un vecteur en fonction du type de données utilisé.]] Les vecteurs sont stockés dans des '''registres vectoriels''', aussi appelés '''registres SIMD'''. Un registre vectoriel peut contenir un vecteur complet, pas plus. En conséquence, ils ont une taille assez importante : ils font généralement 128,,256, voire 512 bits, comparé aux 32/64 bits des registres scalaires. Le résultat est que les processeurs SIMD modernes ont des registres SIMD séparés des registres entiers/flottants normaux. Un défaut de cette organisation est que les registres vectoriels sont des registres en plus, qui doivent être sauvegardés lors des commutations de contexte, lors des interruptions, lors des appels systèmes, etc. {| |+ Comparaison entre un processeur sans registres vectoriels, et avec registres vectoriels. |[[File:Non-SIMD cpu diagram1.svg|vignette|upright=1.5|CPU Non-SIMD]] |[[File:SIMD cpu diagram1.svg|vignette|upright=1.5|CPU SIMD]] |} Les instructions SIMD peuvent être rassemblées en deux grands groupes : les horizontales et les verticales. Les '''instructions SIMD verticales''' travaillent en parallèle sur les éléments qui sont "à la même place" dans deux vecteurs. Elles peuvent additionner ou multiplier deux vecteurs, par exemple. Pour prendre l'exemple d'une instruction d'addition vectorielle, celle-ci va additionner ensemble les données qui sont à la même place dans deux vecteurs, et placer le résultat dans un autre vecteur, à la même place. Les '''instructions SIMD horizontales''' partent d'un vecteur et ont pour résultat un simple nombre. Elles peuvent calculer la somme ou le produit des éléments d'un vecteur, renvoyer le nombre d’éléments nuls dans un vecteur, etc. [[File:Instructions SIMD.png|centre|vignette|upright=2.5|Instructions SIMD]] Les instructions SIMD sont difficilement utilisables dans des langages de haut niveau et c'est donc au compilateur de traduire un programme avec des instructions vectorielles. Les transformations qui permettent de traduire des morceaux de programmes en instructions vectorielles (déroulage de boucles, strip-mining) portent le nom de vectorisation. ===Les instructions SIMD arithmétiques verticales=== Les instructions SIMD sont essentiellement des instructions arithmétiques, comme des additions, des soustractions, des multiplications, éventuellement des opérations bit à bit, et quelques autres. Suivant la taille des données, et le type de celle-si, on devra effecteur des instructions différentes. Par exemple, on devra utiliser deux instructions d'addition différentes suivant qu'on manipule des flottants 64 bits ou des entiers 32 bits. De même, l'instruction pour additionner des vecteurs d'entiers de 16 bits sera différente de celle manipulant des vecteurs d'entiers de 32 bits. L'addition et la multiplication peuvent générer des débordements d'entier. Pour les instructions non-SIMD, le débordement d'une addition est géré par un bit de retenue, stocké dans le registre d'état. Mais pour les instructions SIMD, ce n'est pas une solution très facile à implémenter. À la place, les additions et soustractions utilisent généralement l'arithmétique saturée, à savoir que lorsqu'une addition déborde, le résultat est la valeur maximale représentable dans un entier. Pour la multiplication, les instructions non-SIMD génère un résultat qui est codé sur le double du nombre de bits. Une multiplication de deux nombres 64 bits donnera un résultat sur 128 bits, par exemple. Pour gérer cela, les multiplications non-SIMD stockent ce résultat dans deux registres, ou ne conservent que les 64 bits de poids faible. D'autres solutions sont possibles, mais ces deux là sont les plus utilisées. Pour les instructions SMID, une autre solution est préférée car plus simple pour de telles instructions : fournir deux instructions : une qui calcule les 64 bits de poids faible du résultat, une autre pour les 64 bits de poids fort. Les processeurs SIMD étant utilisés pour du traitement d'image ou du rendu 2D/3D, ils supportent généralement des opérations mathématiques assez complexes sur des nombres flottants. Il n'est pas rare d'avoir des instructions SIMD pour le calcul de l'inverse d'un nombre, de sa racine carrée, l'inverse d'une racine carrée, des fonctions trigonométriques, etc. L'implémentation matérielle de telles instructions est généralement très complexe et gourmande en circuits, ce qui fait que les concepteurs de processeurs rusent. Sauf sur certains processeurs assez peu fréquents, les instructions mathématiques complexes ne calculent pas un résultat exact, mais une approximation du résultat. Utiliser un résultat approximatif ne pose pas de problème pour de telles applications, le rendu d'image ou 3D s'en accommodant parfaitement, contrairement au calcul scientifique. ===Les instructions SIMD de manipulation de données intra-vecteur=== Après avoir vu les instructions SIMD verticales, il est temps de voir les instructions SIMD horizontales. Pour rappel, celles-ci travaillent un vecteur unique, dont elles réarrangent ou modifient les éléments. Tout cela sera plus parlant une fois qu'on aura donné quelques exemples. Mais avant toute chose, nous allons séparer les instructions horizontales en deux sous-types, fondamentalement différents. Le premier sous-type change de place les données dans un vecteur. Le second type est celui des instructions horizontales arithmétiques/logiques habituelles, qu'on verra plus tard. La distinction entre les deux est très importante, comme on le verra plus tard. [[File:SIMD instruction data movement - One operand.png|vignette|upright=1.5|Instructions SIMD de mouvement de données à une opérande.]] Les instructions SIMD horizontales les plus communes bougent des données à l'intérieur des vecteurs, ce qui leur vaut le nom d''''instructions de manipulation de vecteur'''. Les plus simples sont celles qui ont une seule opérande, à savoir qui prennent un vecteur comme opérande et renvoient un vecteur résultat. Les plus intuitives sont les instructions de '''permutation''', dont le nom est assez explicite. Elles changent de place des données dans un vecteur. Dans le cas le plus compliqué, elles prennent deux vecteur : le vecteur dans lequel faire la permutation, et un vecteur qui indique comment faire la permutation. Le vecteur précise, pour chaque élement du vecteur, où il doit aller après la permutation. L'instruction effectue la permutation des entiers/flottants/octets dans le vecteur. Les instructions '''''compress''''' et '''''expand''''' sont elles aussi spécifiques aux vecteurs. L'instruction ''compress'' prend un vecteur en opérande, sélectionne certaines valeurs présentes dedans, et les stocke dans un vecteur de sortie. Elle regroupe les valeurs sélectionnées dans le début du vecteur, et laisse les cases inoccupées à 0. Par exemple, pour un vecteur de 16 entiers, elle permet de sélectionner 5 entiers dedans, et les place dans un vecteur résultat qui ne contient que ces 5 valeurs au début du vecteur, le reste est à 0. L'instruction ''expand'' fait l'inverse : elle prend un vecteur crée par l'instruction ''compress'' (ou du moin qui a le même format), prend toutes les valeurs non-nulles dedans, et les disperse dans un vecteur de sortie, en les mettant à la place désirée. Les autres instructions SIMD horizontales prennent deux opérandes, voire aucune opérande vectorielle (elles prennent un nombre, pas un vecteur). Les instructions '''''shuffle''''' prennent plusieurs vecteurs, sélectionnent les éléments adéquats dans chaque vecteur, et les regroupe dans un vecteur résultat. Par exemple, prenons 2 vecteurs de 16 entiers chacun. Une instruction ''shuffle'' peut prendre 8 entiers dans chaque vecteur et les regrouper dans un vecteur résultat unique de 16 entiers. Ou encore, elle peut prendre trois vecteurs de 16 flottants chacun, prendre 4 flottants dans le premier, 10 dans le second et 2 dans le troisième, et regrouper le tout dans un vecteur résultat de 16 flottants. Il arrive que les deux cas précédents correspondent à des cas séparés, à deux instructions de type ''shuffle'', mais qui fonctionnent différemment. Le premier type prend les N premiers éléments du premier vecteur, puis prend les élements manquant à partir de la fin du vecteur. Le second type prend un élément sur deux dans chaque vecteur, mais avec un décalage d'un rang entre les deux vecteurs. Il y a aussi les instructions dites de '''''broadcast''''', qui copient une valeur unique dans un vecteur entier. Elles sont surtout utilisées pour initialiser des tableaux avec une valeur unique, ou pour remplir un vecteur avec des données prédéterminées pour certains calculs impliquant des constantes. [[File:SIMD instruction exemple.png|centre|vignette|upright=1.5|Instructions SIMD de mouvement de données qui ne sont pas à une opérande.]] ===Les instructions SIMD de réduction=== Il existe des instructions horizontales de type arithmétiques. La plus simple est celle qui additionne les différents entiers/flottants d'un vecteur. Elle est utilisée pour accélérer une opération précise : faire la somme d'un tableau d'entier/flottants. On peut aussi citer les opérations maximum et minimum, qui renvoient le plus grand/petit élément d'un vecteur. Un point important avec ces opérations est qu'elles prennent un vecteur, mais renvoie un résultat unique, un scalaire, un nombre entier/flottant seul. Elles sont parfois appelées des '''instructions de réduction'''. Les instructions de réduction sont généralement la spécificité des processeurs vectoriels, les processeurs SIMD récents n'en ont pas. La raison est que leur implémentation matérielle est généralement assez compliquée. Contrairement aux instructions verticales, elles ont une forme de parallélisme de donnée très limitée. Elles ne travaillent pas exactement sur des données indépendantes, elles font des calculs dont le résultat est utilisé par d'autres, il y a des dépendances de données entre éléments du vecteur. Cependant, il faut savoir que l'on peut émuler une instruction de réduction en utilisant des instructions verticales couplées à des instructions de manipulation de vecteur. Par exemples, prenons le cas d'une instruction de réduction qui additionne entre eux les éléments d'un vecteur. On peut l'émuler en utilisant une addition verticale entre deux vecteurs, et une instruction ''compress''. L'idée est la suivante : on coupe le vecteur initial en deux avec deux instructions ''compress'', ce qui donne deux sous-vecteurs. Puis, on additionne les deux vecteurs résultat entre eux. Puis on répète les deux étapes précédentes jusqu'à obtenir la somme finale. Le fait que l'on puisse émuler les instructions de réduction est la raison principale qui explique que les processeurs SIMD récents n’intègrent pas d'instructions de réduction. Ils se débrouillent avec des instructions SIMD de manipulation de vecteur, et des instructions SIMD verticales. C'est un bon compromis entre cout en circuits et performance. ===Les accès mémoire=== La gestion des accès mémoire est assez hétéroclite, la façon de faire étant différente selon les architectures. Dans le cas le plus simple, les données d'un vecteur sont contigües en mémoire et les instructions ont juste à préciser l'adresse mémoire du début du vecteur, qui est généralement dans un registre d'adresse spécialisé, ou un registre non-SIMD. Avec des vecteurs de 8 octets, toute instruction d'accès mémoire de ce type va lire ou écrire des blocs de 8 octets. L'adresse de départ de ces blocs est soumise à des contraintes d'alignement sur les jeux d'instructions comme le SSE, le MMX, etc. La raison à cela est que gérer des accès non alignés en mémoire rend les circuits de lecture/écriture en mémoire plus complexes. En contrepartie, ces contraintes compliquent l'utilisation des instructions SIMD par le compilateur. D'autres modes d'adressage des vecteurs permettent à une instruction de charger des données dispersées en mémoire pour les rassembler dans un vecteur. On peut notamment citer l'existence d'accès mémoires en stride et en scatter-gather. L''''accès en stride''' regroupe des données séparées par un intervalle régulier d'adresses. Ce mode d'accès a besoin de l'adresse initiale, de celle du premier élément du vecteur, et de la distance entre deux données en mémoire. Il permet aux instructions de mieux gérer les tableaux de structures, ainsi que les tableaux multi-dimensionnels. Lorsqu'on utilise de tels tableaux, il arrive assez souvent que l'on n'accède qu'à des éléments tous séparés par une même distance. Par exemple, si on fait des calculs de géométrie dans l'espace, on peut très bien ne vouloir traiter que les coordonnées sur l'axe des x, sans accès sur l'axe des y ou des z. Les instructions d'accès mémoire en enjambées gèrent de tels cas efficacement. Les processeurs SIMD incorporent aussi les accès en '''''scatter-gather'''''. De tels accès prennent en opérande un vecteur contenant des adresses mémoires, et lit/écrit chacune de ses adresses mémoire indépendamment. Les accès en ''scatter-Gather'' peuvent être vus comme une généralisation de l'adressage indirect à registre aux vecteurs, chaque élément du vecteur étant adressé via adressage indirect. Les accès en ''gather'' sont des lectures : elles prennent un vecteur d'adresse, lisent chaque adresse, et regroupent toutes les données lues dans un vecteur, dans un registre vectoriel. Les accès en ''scatter'' sont l'équivalent pour les écritures. Ils prennent un vecteur d'opérande pour les données à écrire, un vecteur pour les adresses. [[File:Cuda4.png|centre|vignette|upright=2|Adressage en scatter-gather]] Un autre version des accès en ''scatter-gather'' n'utilise pas des adresses mémoire, amis des indices. Le vecteur opérande ne contient pas des adresses, mais des indices qui sont combinés à une adresse de base pour calculer plusieurs adresses. Un tel mode d'adressage permet de faciliter l'implémentation de certaines structures de données, appelées des vecteurs de Liffe. Ils sont très utilisés pour gérer les matrices creuses, des matrices où une grande partie des éléments sont nuls et où, dans un souci d'optimisation, seuls les éléments non nuls de la matrice sont stockés en mémoire. Lors d'une instruction de ''scatter'' ou de ''gather'', tous les accès mémoire n'ont pas lieu en même temps, certains accès mémoire seront terminés avant les autres. Les accès peuvent se faire dans le désordre et de nombreuses optimisations matérielle en profitent pour gagner en performance. Il existe cependant un cas où les accès mémoire doivent se faire dans un ordre bien précis, généralement du premier élément vers le dernier : lorsqu'une instruction ''scatter'' effectue plusieurs écritures à la même adresse. C'est parfaitement possible, et c'est le seul cas où il est intéressant de forcer un ordre pour les écritures. En théorie, une instruction SIMD est un tout, c'est à dire qu'elle ne doit pas modifier les registres SIMD avant que tous les accès mémoire se soient terminés. Cependant, il est possible qu'une partie des accès mémoire déclenchent des défauts de page ou lèvent des exceptions matérielles, alors que les autres se sont terminés. En théorie, le processeur est censé se débarrasser du résultat des accès mémoire terminés. Mais ce serait un gachis de performance, ce qui fait que le processeur viole la règle voulant que les registres SIMD soient mis à jour en une seule fois à la fin de l'instruction de ''scatter-gather''. Dès qu'un accès mémoire se termine, il écrit son résultat dans le registre SIMD, à l'endroit adéquat. Cependant, ce comportement pose un problème. Lorsqu'une exception survient, comme lors d'un défaut de page, le processeur exécute la routine d'interruption associée, puis redémarre l'instruction fautive, le second essai étant le bon. Ici, le processeur ne réexecute pas l'instruction à l'identique, mais seulement les accès non-terminés. Mais comment le processeur sait-il quelles accès mémoire redémarrer ? Il doit pour cela mémoriser quels sont les accès mémoire terminés. Il utilise pour cela un registre architectural appelé le '''masque de complétion'''. Le registre est forcément architectural car il doit être préservé lors d'un changement de contexte, l’exécution de l'exception matérielle forçant un changement de contexte. ===Exemple avec les processeurs x86=== [[File:PD-20060908-SSE3-01.svg|vignette|Chronologie des extensions x86 SIMD.]] Après avoir vu la théorie sur les instructions SIMD, il est temps de voir un exemple concret : celui des instructions SIMD des processeurs x86, présents dans nos PC. Le jeu d'instruction des PC qui fonctionnent sous Windows est appelé le x86. C'est un jeu d'instructions particulièrement ancien, apparu en 1978. Depuis, des '''extensions x86''' ont ajouté des instructions au x86 de base. On peut citer par exemple les extensions MMX, SSE, SSE2, ''3dnow!'', etc. Les premières instructions SIMD furent fournies par une extension x86 du nom de MMX, introduite par Intel en 1996 sur le processeur Pentium MMX. Le MMX a perduré durant quelques années avant d'être remplacé par les extensions SSE. La même année, AMD sorti d'expansion ''3DNow!'', qui ajoutait 21 instructions SIMD similaires à celles du MMX. Celui-ci fût suivi du ''3DNow!+'' quelques années plus tard. Le SSE fût ensuite décliné en plusieurs versions avant d'être "remplacé" par l'AVX. Une grande quantité de ces extensions x86 sont des ajouts d'instructions SIMD. L’'''extension MMX''' ajoutait pas mal d'instructions SIMD assez basiques, essentiellement des instructions arithmétiques : addition, soustraction, opérations logiques, décalages, rotations, mise à zéro d'un registre, etc. La multiplication est aussi supportée, mais avec quelques petites subtilités, via l'instruction PMULLW. Les instructions MMX ne mettent pas le registre d'état à jour et ne préviennent pas en cas d'overflow ou d'underflow si ceux-ci arrivent (pour les instructions qui ne travaillent pas en arithmétique saturée). Le MMX introduisait 8 registres vectoriels, du nom de MM0, MM1, MM2, MM3, MM4, MM5, MM6 et MM7. Ils ne pouvaient contenir que des nombres entiers et faisaient 64 bits. Ils avaient cependant un léger défaut, qui a nui à l'adoption du MMX. Pour rappel, avant le MMX, les flottants étaient gérés par l'extension x87, qui définit 8 registres flottants de 80 bits. Et chaque registre MMX correspondait aux 64 bits de poids faible d'un registre flottant x87 ! Le système d'''alias'' de registres typique des CPU x86 a encore frappé ! En conséquence, il était impossible d'utiliser en même temps l'unité de calcul flottante et l'unité MMX. Par contre, sauvegarder les registres lors d'un changement de contexte, d'une interruption, ou d'un appel de fonction était très simple : la sauvegarde des registres de la FPU x87 suffisait. [[File:MMX-FPU-Register.JPG|centre|vignette|upright=2|Registres MMX et FPU x87.]] [[File:XMM registers.svg|droite|vignette|XMM registers]] Dans les années 1999, une nouvelle extension SIMD fit son apparition sur les processeurs Intel Pentium 3 : le '''Streaming SIMD Extensions''', abrévié SSE. Ce SSE fut ensuite complété, et différentes versions virent le jour : le SSE2, SSE3, SSE4, etc. Cette extension fit apparaitre 8 nouveaux registres, les registres XMM. Sur les processeurs 64 bits, ces registres sont doublés et on en trouve donc 16. En plus de ces registres, on trouve aussi un registre d'état qui permet de contrôler le comportement des instructions SSE : celui contient des bits qui permettront de dire au processeur que les instructions doivent arrondir leurs calculs d'une certaine façon, etc. Ce registre n'est autre que le registre MXCSR. Chose étrange, seuls les 16 premiers bits de ce registre ont une utilité : les concepteurs du SSE ont surement préférés laisser un peu de marge au cas où. La première version du SSE contenait assez peu d'instructions : seulement 70. Le SSE première version ne fournissait que des instructions pouvant manipuler des paquets contenant 4 nombres flottants de 32 bits (simple précision). Je ne vais pas toutes les lister, mais je peux quand-même dire qu'on trouve des instructions arithmétiques de base, avec pas mal d'opérations en plus : permutations, opérations arithmétiques complexes, autres. Petit détail : la multiplication est gérée plus simplement et l'on a pas besoin de s’embêter à faire mumuse avec plusieurs instructions différentes pour faire une simple multiplication comme avec le MMX. On peut quand même signaler une chose : des instructions permettant de contrôler le cache firent leur apparition. On retrouve ainsi des instructions qui permettent d'écrire ou de lire le contenu d'un registre XMM en mémoire sans le copier dans le cache. Ces instructions permettent ainsi de garder le cache propre en évitant de copier inutilement des données dedans. On peut citer par exemple, les instructions MOVNTQ et MOVNTPS du SSE premiére version. On trouve aussi des instructions permettant de charger le contenu d'une portion de mémoire dans le cache, ce qui permet de contrôler son contenu. De telles instructions de prefetch permettent ainsi de charger à l'avance une donnée dont on aura besoin, permettant de supprimer pas mal de cache miss. Le SSE fournissait notamment les instructions PREFETCH0, PREFETCH1, PREFETCH2 et PREFETCHNTA. Autant vous dire qu'utiliser ces instructions peut donner lieu à de sacrés gains si on s'y prend correctement ! Il faut tout de même noter que le SSE n'est pas seul "jeu d'instruction" incorporant des instructions de contrôle du cache : certains jeux d'instruction POWER PC (je pense à l'Altivec) ont aussi cette particularité. Avec le '''SSE2''', de nouvelles instructions furent ajoutés, permettant d'utiliser des nombres de 64, 16 et 8 bits dans chaque vecteur. Le SSE2 incorporait ainsi pas moins de 144 instructions différentes. Ce qui commençait à faire beaucoup. Puis, vient le '''SSE3''', avec ses 13 instructions supplémentaires. Pas grand-chose à signaler, si ce n'est que des instructions permettant d'additionner ou de soustraire tous les éléments d'un paquet SSE ensemble, des instructions pour les nombres complexes, et plus intéressant : les deux instructions MWAIT et MONITOR qui permettent de paralléliser plus facilement des programmes. Le '''SSE4''' fut un peu plus complexe et fut décliné lui-même en 2 versions. Le SSE4.1 introduit ainsi des opérations de calcul de moyenne, de copie conditionnelle de registre (un registre est copié dans un autre si le résultat d'une opération de comparaison précédente est vrai), de calcul de produits scalaire, de calcul du minimum ou du maximum de deux entiers, des calculs d'arrondis, et quelques autres. Avec le SSE4.2, le vice à été poussé jusqu'à incorporer des instructions de traitement de chaines de caractères. [[File:AVX registers.svg|vignette|Registres AVX.]] Avec l''''AVX (Advanced Vector eXtensions)''', on retrouve 16 registres d'une taille de 256 bits, nommés de YMM0 à YMM15 et dédiés aux instructions AVX. Ils sont partagés avec les registres XMM : les 128 bits de poids faible des registres YMM ne sont autres que les registres XMM. L'AVX complète le SSE et ses extensions, en rajoutant quelques instructions, et surtout en permettant de traiter des données de 256 bits. Son principal atout face au SSE est que les instructions AVX permettent de préciser le registre de destination en plus des registres d'opérandes. Avec le SSE et le MMX, le résultat d'une instruction SIMD était écrit dans un des deux registres d'opérande manipulé par l'instruction. Il fallait sauvegarder son contenu si on en avait besoin plus tard, ce qui n'est plus nécessaire avec l'AVX. ==La performance des processeurs SIMD== Il est intéressant de comparer la performance d'un processeur SIMD et celle d'un processeur normal, sans SIMD. Mais cet exercice est compliqué par le fait que les processeurs non-SIMD sont très nombreux : entre les CPU superscalaires, à émission dans l'ordre, exécution dans le désordre, ceux sans rien de tout cela, on a de quoi être perdu. Dans ce qui suit, nous allons comparer deux processeurs basiques sans pipeline : un avec SIMD, un autre sans. Comparé à un processeur sans pipeline, on s'attend à ce que la performance soit augmentée d'un facteur N, avec N le nombre maximal d'entiers/flottants que l'on peut mettre dans un vecteur. Si une architecture SIMD fait N calculs en parallèles, alors les performances sont censées être multipliées par N. Cela laisse penser que plus les vecteurs sont longs, plus le gain en performance est important. Il s'agit là d'un résultat basique, mais la vraie vie est différente. Un premier problème est que ce résultat ne tient que si la mémoire RAM et les caches suivent. En effet, faire N calculs en parallèle demande de lire/écrire N fois plus de données. Les données sont souvent stockées dans des registres vectoriels, ce qui fait que la pression sur le banc de registre est assez importante. Mais cela signifie aussi que les instructions d'accès mémoire doivent lire/écrire N fois plus de données. Il faut alors ajouter des ports sur le cache, élargir le bus mémoire, augmenter la taille des lignes de cache, etc. Le débit binaire des caches et de la mémoire RAM deviennent rapidement des points limitants, qui réduisent le gain en performance des instructions SIMD. Et ne parlons pas de l'interaction avec la mémoire virtuelle : vu qu'on lit/écrit par paquets de données, cela signifie qu'on traverse une page mémoire plus vite, ce qui fait que les défauts de page sont lus fréquents. Un processeur SIMD a intérêt à être combiné à une RAM et des caches solides, très performants. C'est le cas sur les processeurs modernes, mais cela a pose des problèmes pour les processeurs vectoriels, et a mené à leur abandon progressif. Mais passons outre ce problème et regardons la seconde raison qui font que ce résultat est naïf : la loi d'Amdhal ! Toutes les portions d'un programme ne sont pas accélérées par des instructions SIMD, il y a des portions dont les calculs ont des dépendances de données, d'autres qui ne sont pas parallélisables, etc. Une partie du programme est donc impossible à véctoriser (i.e optimiser pour le SIMD), une l'autre l'est. Pour simplifier les explications, on suppose qu'une instruction SIMD prend le même nombre de cycles que son équivalent non-SIMD. Par exemple, une addition SIMD prend le même temps qu'une addition normale. Dans ce cas, la portion non-vectorisable du code reste inchangée, alors que la portion vectorisable est divisée par N, par la largeur du vecteur. On retombe sur une formulation identique à la loi d'Amdhal sur le fond. Une manière de quantifier le tout est d'utiliser la métrique de l'efficience SIMD. Elle est calculée en divisant deux grandeurs. La première est elle-même un rapport : c'est le gain obtenu en terme de nombre d'instructions exécutées. Prenons un programme qui exécute I instructions avant vectorisation, et qui en exécute I/X après. Le gain s'exprime comme suit : : <math>G = \frac{I}{I/X} = X</math> Maintenant, prenons ce gain est divisons-le par la largeur d'un vecteur. On obtient alors l'efficience SIMD : : <math>\text{Efficience SIMD} = \frac{G}{N} = \frac{\frac{I}{I/X}}{N}</math> Notons que le gain et la largeur SIMD ne sont égales que dans un cas bien précis : tout le code est vectorisable. Il s'agit donc d'une reformulation du gain de la loi d'Amdhal, mais dans laquelle le pourcentage de code série est caché. Il est intéressant de regarder l'efficience SIMD quand on augmente la portion du code série, ainsi que la taille des vecteurs. Prenons un cas assez généreux, réaliste pour certaines applications très adaptées au SIMD : 1% du code est non-vectorisable, 99% profite du SIMD. L'efficience SIMD varie alors assez rapidement avec la taille des vecteurs. De 99% pour N=2, elle descend à 98% pour N=16, et tombe sous les 50% pou N=128. La conséquence est que les très grandes tailles de vecteurs ne sont pas vraiment utiles, le rendement est décroissant avec la taille des vecteurs. ==Les processeurs SIMD à registres généraux== Le premier type de processeur SIMD que nous allons voir est celui des processeurs de type '''SWAR''' (''SIMD Within A Register''). Le terme SWAR est un terme polysémique dont le sens a beaucoup changé au fil du temps. Ici, nous allons l'utiliser dans la définition la plus stricte : celle où on effectue du SIMD dans les registres généraux du processeur, sans registres spécialisés. Il s'agit d'une forme de SIMD qui est aujourd'hui peu utilisée, mais qui a été la première forme de SIMD utilisée dans des processeurs grand public commerciaux. L'idée est d'améliorer un petit peu un processeur normal, non-SIMD. Un processeur usuel contient des registres généraux de 32 à 64 bits, qui stockent chacun un opérande de même taille que le registre. D'anciens processeurs avaient des instructions pour effectuer des calculs simples, sur des opérandes plus courts. Par exemple, les processeurs x86 32 bits sont capables de faire des calculs sur 8, 16, ou 32 bits. Mais on ne pouvait placer qu'un seul opérande de 8, 16 ou 32 bits dans un registre général, ce qui est un léger gâchis. Le SWAR est une amélioration de cette technique qui vise à mieux utiliser les registres et l'ALU. L'idée est de regrouper plusieurs opérandes de 8, 16, voire 32 bits, dans un seul registre général. De plus, on ajoute des instructions SIMD d'addition/soustraction/autres, qui lisent des opérandes 8/16/32 bits dans ces registres généraux. Par exemple, sur un processeur 32 bits, on peut ajouter une opération d'addition qui lit deux registres de 32 bits, pour récupérer quatre opérandes de 16 bits (deux par registre) et effectue deux additions 16 bits en même temps dans l'ALU. Et il y a la même chose pour d'autres opérations, comme la soustraction, la comparaison, etc. Les instructions SIMD disponibles sur ces processeurs se limitent généralement à des additions, soustractions, comparaisons, mais guère plus. De plus, il s'agit d'instructions entières, il n'y a pas d'instructions SIMD flottantes. La raison à cela est très simple, mais nous l'expliquerons plus bas, dans la section sur l'implémentation. Disons simplement que de tells architectures visent une économie en circuit maximale. Elles implémentent les instructions SIMD en utilisant le moins de circuits possibles, ce qui fait qu'elles réutilisent les registres généraux et n'ont pas de registres SIMD séparés. ===Les extensions/jeux d'instructions de type SWAR=== Les premiers processeurs à intégrer du SWAR étaient les processeurs DEC Alpha, avec l'extension multimédia '''''Motion Video Extensions'''''. Les processeurs en question étaient les processeurs Alpha 21164PC (PCA56 and PCA57), Alpha 21264 (EV6) and Alpha 21364 (EV7). Les instructions SIMD disponibles étaient très simples et se résumaient à quelques comparaisons : trouver le maximum ou le minimum de deux opérandes de 8/16 bits, quelques instructions de permutation. Les extensions '''''Multimedia Acceleration eXtensions''''' des processeurs Hewlett-Packard PA-RISC sont aussi de ce type, mais disposent de plus d'instructions SIMD. La première version, appelée MAX-1, était disponible sur les processeurs 32 bits de la marque, à partir du processeur PA-7100LC. Les instructions disponibles étaient des additions et soustractions d'opérandes 16 bit. Il y avait en tout trois instructions d'addition et trois pour la soustraction. En tout, il y avait : * une instruction d'addition/soustraction non-signée utilisant l'arithmétique modulaire ; * une instruction d'addition/soustraction signée en arithmétique modulaire ; * une instruction d'addition/soustraction signée en arithmétique saturée. Outre les additions/soustractions, il y avait aussi une instruction pour calculer la moyenne de deux opérandes 16 bits, ainsi qu'une addition fusionnée avec un décalage. La seconde version, appelée MAX-2, ajouta des instructions d'additions fusionnées avec des décalages, mais aussi des instructions de permutation. De plus, cette version était disponible sur les processeurs 64 bits de la marque, ce qui fait que les registres étaient eux aussi de 64 bits. Ils pouvaient contenir deux fois plus d'opérandes 16 bits. Il n'y avait de gestion d'opérandes 32 bits pour les extensions SIMD. ===L'implémentation matérielle=== L'avantage de cette technique est la grande simplicité d'implémentation. On n'ajoute pas de registres SIMD séparés, ce qui a de nombreux avantages : économie de circuits car pas besoin d'un second banc de registre, pas besoin de sauvegarder des registres SIMD en plus lors d'un appel système/interruption, etc. L'implémentation a juste besoin de modifier le décodeur et l'unité de calcul. Le décodeur est modifié pour ajouter des instructions en plus, rien de spécifique au SWAR. Les modifications de l'unité de calcul sont spécifiques au SWAR et modifient la manière dont elle gère les retenues. Une première solution est d'utiliser une ALU bit-slicée, ce qui est l'idéal pour les unités de calcul entières. Par exemple, pour une ALU 32 bits entière, on peut la découper en 4 unités de calcul 8 bits. Pour une opération SIMD avec 4 opérandes 8 bits, les quatre ALU fonctionnent en parallèle, il n'y a pas transmission des retenues. Pour les calculs SIMD avec des opérandes de 16 bits, on regroupe les ALU par paire et on propage les retenues à l'intérieur d'une paire, mais pas entre les paires. Enfin, pour un calcul non-SIMD, les opérandes de 32 bits, on propage les retenues normalement, d'une ALU 8 bits vers la suivante. Une autre solution prend un additionneur 32/64 bits normal, mais mets à 0 les retenues. L'implémentation est très simple avec un additionneur à anticipation de retenues, où les retenues sont calculées en avance, avant de faire l'addition proprement dite, par un circuit d'anticipation de retenue. Il suffit alors d'ajouter un circuit de masquage en sortie du circuit d'anticipation de retenue, qui met à zéro les retenues adéquates. La gestion des débordements demande d'ajouter des circuits, ou du moins de modifier ceux déjà présents dans l'ALU. Typiquement, les circuits pour gérer l'arithmétique saturée sont un peu modifiés pour effectuer la mise à 1111...111 octet par octet et chaque octet est masqué si besoin. [[File:ALU modifiée pour implémenter du SWAR.png|centre|vignette|upright=3|ALU modifiée pour implémenter du SWAR]] Notons que la technique ne s'applique qu'aux additions, et aux opérations dérivées comme la soustraction et les comparaisons (ces dernières sont des soustractions déguisées). Mais les multiplications ou divisions ne peuvent pas s'implémenter simplement en modifiant des ALU existantes, du moins pas avec des modifications simples. Aussi, les processeurs qui utilisent cette technique se bornent aux additions et opérations dérivées, pas plus. La gestion des opérations de permutation est quant à elle très simple : beaucoup de processeurs disposent déjà d'instructions de permutation d'octets, qui sont l'équivalent d'instructions SIMD de permutation pour les registres généraux. Nous en avions déjà parlé dans le chapitre sur le langage machine et l'assembleur, quand nous avions fait la liste des instructions les plus courantes. De telles instructions de permutation d'octet sont utiles pour gérer le boutisme ou effectuer quelques manipulations assez rares. Pas besoin de rajouter une ALU dédiée, celle-ci est déjà présente de base, du moins si les instructions de permutation d'octet sont déjà présentes. ==Les processeurs SIMD purs (''packed SIMD'')== Maintenant que nous avons vu les instructions SIMD, passons maintenant aux processeurs eux-même. Tous les processeurs que nous allons voir dans ce qui suit supportent des instructions SIMD. Mais leur implémentation matérielle, leur micro-architecture, n'est pas la même. Ils ont aussi quelques différences en termes de jeu d'instruction, mais qui sont fortement liées à l'implémentation matérielle. Nous allons ici voir les '''processeurs SIMD purs''', aussi dits de type ''Packed SIMD''. Ils implémentent des instructions SIMD sans rien de plus, juste le strict minimum : pas de prédication, pas de vecteurs de taille variable, presque pas d’instructions horizontales, architecture de type LOAD-STORE. De tels processeurs utilisent plusieurs ALU entières/flottantes qui travaillent en parallèle. De plus, ils disposent de registres SIMD séparés des registres généraux. Voyons ces points immédiatement. * La première distinction est que les vecteurs sont stockés dans des registres séparés des registres généraux, appelés les '''registres SIMD''', ou ''registres vectoriels''. Ils sont beaucoup plus larges que les registres classiques et mémorisent 2, 4, 8 entiers/flottants. * La seconde distinction est que les calculs SIMD sont effectués dans une unité SIMD séparée de l'ALU ou de la FPU. L'unité de calcul SIMD peut exécuter au minimum des instructions SIMD verticales, plus rarement des instructions SIMD horizontales. Pour ce faire, elle regroupe plusieurs additionneurs/multiplieurs, qui effectuent des calculs en parallèle. ===Les instructions SIMD verticales : plusieurs ALU travaillant en parallèle=== [[File:SIMD2.svg|vignette|Parallélisme de données au niveau de l'unité de calcul. Celle-ci contient plusieurs circuits indépendants qui appliquent la même opération sur des données différentes.]] Une unité de calcul SIMD gère au minimum des instructions SIMD verticales. Et pour ce faire, elle contient plusieurs additionneurs/multiplieurs séparés. Par exemple, pour additionner deux vecteurs contenant chacun 16 entiers/flottants, il faut utilise 16 additionneurs entiers et 16 additionneurs flottants. Dans le cas général, une ALU SIMD est composée de plusieurs ALU entières et flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. Notons que les ALU travaillent en parallèle, elles font des calculs indépendants. Notons que les additionneurs dans chaque ALU doivent pouvoir être configurés de manière à gérer des tailles de données différentes. Par exemple, si on prend un vecteur simple, qui peut contenir soit 32 entiers de 16 bits, soit 16 entiers 32 bits, soit 8 entiers 64 bits, alors on doit utiliser 8 additionneurs, mais chacun d'entre eux doit pouvoir être reconfiguré de manière à ne pas propager les retenues au-delà des premiers 16 ou 32 bits. Un défaut de cette organisation est que le cout en circuits est loin d'être négligeable. Il faut dupliquer des unités de calcul et les coller en rajoutant des circuits, cela utilise beaucoup de transistors. Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures. Retenez cependant : l'usage de plusieurs ALU travaillant en parallèle a plusieurs défauts. Premièrement, cela implique des vecteurs de taille fixe, du fait que le nombre d'ALU travaillant en parallèle est fixe. Deuxièmement, des difficultés d'implémentation concernant les instructions de réduction. Détaillons ce second point ! Une autre possibilité est d'utiliser moins d'unités de calcul qu'il n'y a d'éléments dans un vecteur. Par exemple, pour des vecteurs contenant 32 flottants, il se peut qu'il n'y ait que 16 ALU. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale. L'avantage est que l'ensemble des calculs se fait en une seule instruction machine. L'implémentation est généralement la suivante : le processeur décode une instruction SIMD en deux micro-instructions SIMD plus courtes. Par exemple, pour une instruction SIMD de 32 éléments, elle sera décodée en deux instructions SIMD de 16 éléments, chacune étant exécutée sur l'ALU. Il est possible de réduire fortement l'impact en performance en doublant la fréquence de l'ALU. Par exemple, pour des vecteurs contenant 32 flottants, le processeur incorpore 16 ALU, mais celles-ci fonctionne à une fréquence double de celle du processeur. L'usage d'une ALU à double fréquence est une technique qui a été utilisée sur le Pentium 4 et quelques processeurs, pour des instructions non-SIMD. Mais pour les processeurs SIMD, elle s'applique à la perfection. L'implémentation est la même que précédemment, sauf que le ''scheduler'' fonctionne à double fréquence. Pour limiter la casse, il est préférable d'utiliser un ''scheduler'' séparé pour les instructions SIMD, qui sera seule à fonctionner à double fréquence. ===Les instructions horizontales : une ALU simple séparée=== Les instructions horizontales posent des difficultés d’implémentation. Elles ne peuvent pas s'implémenter en utilisant plusieurs ALU travaillant en parallèle, contrairement aux instructions verticales, ce qui fait qu'elles utilisent généralement des unités de calcul spécialisées. Et c'est là que la différence entre instructions de manipulation de vecteurs et instructions de réduction vient encore une fois poser problème. Les instructions de manipulation de vecteur sont relativement simples à implémenter : elles ont juste besoin d'une ALU spécialisée, relativement simple, composée de beaucoup de multiplexeurs à configurer convenablement. Mais pour les instructions de réduction, c'est autre chose ! L'implémentation des instructions de réduction est possible mais a un cout en circuits assez prohibitif. Pour additionner N entiers, il faut utiliser un additionneur multi-opérande qui prend beaucoup de circuits et dont le temps de calcul est proche de celui d'une multiplication (très lent). Trouver le maximum et le minimum d'un nombre demandent de faire la même chose avec des comparateurs. Le tout a un cout en circuit non-négligeable, pour accélérer des opérations assez peu fréquentes. Elles sont surtout utilisées pour trouver la somme ou le maximum/minimum d'un tableau, opération séquentielle par nature, avec peu parallélisme de données, peu courante. Une autre solution utilise une unité SIMD normale, mais intègre un système de contournement, de ''bypass'', qui relierait l'entrée d'une ALU à la sortie d'une autre. Mais le cout lié aux interconnexions est très important : on doit relier chaque ALU à toutes les autres dans le pire des cas, on peut optimiser le tout et réduire un peu la quantité d'interconnexions, mais le cout reste important. Au final, on gagne en circuits ce qu'on perd en interconnexions. Le choix entre les deux solutions est loin d'être facile et dépend du processeur, de la technologie utilisée, du budget en transistors disponibles, etc. Les processeurs SIMD purs résolvent ce problème assez simplement : ils n'implémentent pas d'instructions de réduction. Par contre, ils implémentent systématiquement des instructions de manipulation de vecteur, comme des permutations ou autres. La raison est que l'on peut émuler les instructions de réduction en utilisant des instructions SIMD de manipulation de vecteur couplées à des additions/multiplications SIMD verticales. Il s'agit donc d'un compromis entre cout en circuits et performance finale. Niveau circuits, on a une unité SIMD avec plusieurs ALU de calcul en parallèle, une unité de manipulation de vecteur séparée assez simple et au cout en circuits raisonnable. Les performances pour les opérations de réduction sont acceptables, le cout en performance est relativement modéré. ===Le banc de registre vectoriel=== La présence de registres vectoriels séparés permet d'avoir des vecteurs assez longs. Pour ce faire, les registres vectoriels sont plus grands que les registres généraux. Par exemple, sur un processeur 32 bits, qui gère donc des entiers de 32 bits, les vecteurs peuvent faire 128 ou 256 bits. Un vecteur contient donc plusieurs entiers ou flottants de taille maximale. On n'est pas dans le cas précédent, où registres généraux et SIMD sont les mêmes, ils sont séparés et n'ont pas la même taille. Le processeur utilise un banc de registre séparé pour les registres SIMD, appelé le '''banc de registre SIMD'''. Vu que les registres vectoriels sont assez longs, le banc de registre vectoriels a une grande taille, une grande capacité mémoire. Pour donner un exemple, les extensions SSE des processeurs x86 définissent 8 registres SIMD de 128 bits chacun. Ils ont été introduits à une époque où les processeurs avaient des registres généraux de 32 bits. Le banc de registres associé était donc environ 4 fois plus grand que celui pour les registres généraux. Je dis environ car le décodeur avait sensiblement la même taille, seul le plan mémoire était grossit, mais l'idée est là. Une conséquence est que le banc de registres SIMD consomme beaucoup d'énergie et d'électricité. Il en est de même avec les ALU SIMD. Et diverses optimisations tentent de réduire ce cout en énergie. L'une d'entre ajoute une mémoire cache entre les ALU SIMD et les registres vectoriels ! L'idée est que si un registre vectoriel est relu plusieurs fois de suite, la première lecture lira le vecteur dans le banc de registres SIMD, mais les lectures suivantes liront le vecteur dans le cache. L'optimisation marche car accéder au banc de registre vectoriel est vraiment couteux, plus qu'accéder à un cache. Le cache en question est appelé un '''''register reuse caches'''''. Les ''register reuse caches'' sont présents sur le processeur Monaka de Fujitsu, mais aussi et surtout dans les cartes graphiques modernes. Nous ferrons l'impasse sur les GPU modernes, car leur usage est quelque peu particulier. Il ne servent pas que à réduire la consommation d'énergie, mais aussi à réduire les conflits d'accès au banc de registre, qui est en réalité composé de plusieurs banques indépendantes. Leur usage sur le CPU Monaka est plus simple : ils ne servent vraiment qu'à réduire la consommation d'énergie, rien de plus. : Pour ceux qui veulent en savoir plus pour leur utilisation sur les cartes graphiques, tout est détaillé dans mon wikilivre sur les cartes graphiques, via le lien suivant. Mais sachez cependant qu'il faut idéalement lire le chapitre entier, et peut-être même les chapitres précédents, pour comprendre cette section. Voici le lien : [https://fr.wikibooks.org/wiki/Les_cartes_graphiques/La_microarchitecture_des_processeurs_de_shaders#L'Operand_Collector_et_les_caches_de_register_reuse L'Operand_Collector_et_les_caches_de_register_reuse] ===L'implémentation des LOAD-STORE à des données consécutives=== Les architectures SIMD pures sont des architectures de type LOAD-STORE, ce qui veut dire que les instructions SIMD ne peuvent que lire ou écrire dans les registres vectoriels, pas en mémoire ou ailleurs. Les transferts entre registres SIMD et mémoire sont le fait d'instructions LOAD et STORE spécialisées, qui transfèrent des vecteurs entre RAM et registres vectoriels. Les autres architectures n'ont pas nécessairement ce genre de restrictions, mais laissons cela pour plus tard. Implémenter les instructions LOAD/STORE pour des vecteurs dont les données sont consécutives en mémoire n'est pas très compliqué. Il suffit juste d'élargir le port de lecture/écriture du cache L1 de données, pour pouvoir lire/écrire un vecteur entier. Rien de bien compliqué, il suffit juste d'ajouter des interconnexions et d'ajouter des multiplexeurs. Du moins, c'est le cas tant que les vecteurs sont plus petits qu'une ligne de cache, ce qui est souvent le cas en pratique. Dans le cas très rare où les vecteurs sont plus longs qu'une ligne de cache, les LOAD/STORE doivent se faire en deux accès dans le cache, ce qui demande d'ajouter du matériel pour contrôler la situation. Précisons cependant qu'il s'agit là d'un cas idéal, où les vecteurs sont correctement alignés en mémoire. En effet, il se peut qu'un vecteur soit à cheval sur deux lignes de cache, alors qu'il est plus petit qu'une ligne de cache. Il suffit pour cela qu'il soit placé à une adresse adéquate. Par exemple, prenons un vecteur de 16 octets et des lignes de cache de 32 octets. Si le vecteur démarre à l'adresse 24, alors il sera à cheval sur deux lignes de cache. Il faut donc intégrer des circuits pour gérer cette situation. Pour cela, il suffit d'ajouter un registre pour stocker la première de ligne lue, puis un circuit qui combine les deux lignes de cache pour donner le vecteur final. ===L'implémentation des LOAD-STORE en ''scatter-gather''=== L'implémentation des accès en ''scatter-gather'' est plus compliquée. Un accès en ''scatter-gather'' demande de faire des calculs d'adresse, suivis par un ensemble de lectures/écritures, suivis par un regroupement des données lues dans un vecteur. Le calcul d'adresse peut se faire en parallèle dans une unité de calcul SIMD, dans plusieurs unités de calcul d'adresse en parallèle. Par contre, les lectures/écritures ne le peuvent pas. Le cache n'a pas assez de ports de lecture/écriture pour lire/écrire N données. Il a quelques ports, ce qui lui permet de lire/écrire 3/4 données grand maximum, soit moins que ce qui est nécessaire pour faire un accès en ''scatter-gather'' en une fois. Les lectures/écritures sont donc effectuées en plusieurs fois. Pour une opération de ''Gather'', il faut aussi regrouper les données lues dans un vecteur. Sur les caches des processeurs à haute performance, le calcul d'adresse est intégré dans le décodeur. Ce sont des caches adressés par somme, que nous avions abordé dans le chapitre sur les mémoires caches. Avec de telles caches, on ne peut pas utiliser une unité de calcul d'adresse SIMD, on ne peut calculer qu'autant d'adresse qu'il y a de ports sur le cache. Les accès en ''scatter-gather'' regroupent plusieurs accès mémoire, qui peuvent être effectués dans le désordre. La seule exception est celle où un ''scatter'' effectue plusieurs écritures à la même adresse, où les deux écritures doivent se faire dans l'ordre allant du premier au dernier élément du vecteur d'adresse. Les processeurs SIMD profitent de cette absence d'ordre pour effectuer les accès dans un ordre le plus optimisé possible. Par exemple, imaginons le cas où une instruction ''gather'' lise des éléments placés dans deux lignes de cache uniquement, mais dans le désordre en passant sans cesse d'une ligne de cache à l'autre. Il est alors possible de faire tous les accès dans la première ligne de cache en premier, avant de faire ceux allant dans l'autre. Une telle optimisation marche aussi bien pour les lectures que les écritures, pour les ''gather'' que pour les ''scatter''. Elle peut aussi être adaptée pour plus de deux lignes de cache. Elle porte le nom de '''''memory coalescing'''''. Une autre optimisation possible est de détecter le cas où un accès en ''scatter-gather'' accède à des données consécutives. Il suffit pour cela que les indices/adresses du vecteur opérande soient consécutives, et cela arrive plus souvent qu'on ne le pense. Dans ce cas, l'accès est remplacé par une instruction LOAD/STORE SIMD normale, beaucoup plus simple et plus rapide. Les deux optimisations demandent que les adresses soient calculées avant de faire l'accès mémoire. Les adresses calculées sont alors comparées entre elles, par un réseau de comparateurs assez complexe. Un point important de l'implémentation des instructions ''gather'' est qu'une donnée chargée doit être insérée au bon endroit dans le vecteur destination. Et pareil pour les ''scatter'' : il faut lire la bonne donnée au bon moment. Si on prend une ligne de cache complète, il faut lire plusieurs élèments en même temps et les envoyer au registre de destination au bon endroit. Il faut pour cela tout un réseau de multiplexeurs assez complexe pour faire ce travail. Il y a le même genre de réseau pour les instructions ''scatter'', mais qui va dans l'autre sens : du registre vers la ligne de cache. ==Les processeurs SIMD avec prédication== Un obstacle très gênant à la vectorisation est la présence de branchements conditionnels dans les boucles à vectoriser. Si une boucle contient des branchements conditionnels, elle ne peut pas être vectorisée facilement : il est impossible de zapper certains éléments d'un vecteur suivant une condition. Il n'est pas possible d'effectuer une instruction SIMD seulement sur certains éléments d'un vecteur et d'ignorer les autres, en fonction du résultat d'un branchement conditionnel. Tout le vecteur y passe, ou le vecteur est épargné, mais la condition intermédiaire n'est pas possible avec du SIMD simple. Pour résoudre ce problème, les processeurs SIMD incorporent diverses techniques pour implémenter ou éviter les branchements conditionnels le plus possible. La plus simple est l'incorporation de certaines instructions SIMD simples, comme la valeur absolue, le calcul du minimum/maximum, etc. Ces opérations sont généralement émulées en utilisant des branchements, avec quelques conditions simples. Incorporer ces instructions permet ainsi de faire disparaitre une partie des branchements du code. Elle ne paye pas de mine, mais elle a un résultat pas négligeable pour le rendu graphique, certaines applications de calcul scientifiques, et quelques autres. Son cout en transistors est très variable, il faut rajouter des circuits dans le séquenceur, l'unité de calcul SIMD est assez peu modifiée (il faut juste rajouter des multiplexeurs). Le vrai problème est que cette solution laisse énormément de branchements dans le code. Et qu'il faut donc trouver une solution plus générale, capable de réduire drastiquement les branchements. Pour cela, on réutilise une technique vue dans les chapitre sur l'assembleur et le langage machine : la prédication. ===Les instructions à prédicats=== Pour optimiser le cas général, il est possible d'utiliser des instructions à prédicats adaptées pour fonctionner sur des vecteurs. L'idée est que quand une instruction effectue un calcul sur un ou deux vecteurs, certains éléments du vecteurs sont ignorés. Les éléments à ignorer sont choisis suivant le résultat d'une instruction de comparaison, qui effectue un test : les éléments pour lesquels ce test est respecté sont pris en compte, ceux qui ne passent pas le test sont ignorés. Pour donner un exemple d'utilisation, imaginons que l'on ait un vecteur dans lequel on veut remplacer toutes les valeurs négatives par des 0. Dans ce cas, on utilise : * une instruction de comparaison, qui compare chaque élément du vecteur avec 0 et génère plusieurs bits de résultat ; * suivi d'une instruction à prédicat qui met à zéro les éléments pour lesquels les bits de résultat précédents sont à 1. La prédication peut aussi être utilisée pour simuler des vecteurs de taille variable. Tous les vecteurs ont une taille fixe, mais on peut ne pas faire de calculs sur la fin d'un vecteur, ce qui fait que tout se passe comme si le vecteur était plus petit. Ce n'est pas sa seule utilisation, mais la prédication permet de simuler ce comportement. Néanmoins, cela n'en fait pas un processeur vectoriel, capable de traiter des vecteurs de taille variable : il n'y a pas de moyen de préciser explicitement la taille des vecteurs à traiter. Tout cela demande plusieurs modifications du jeu d'instruction : ajouter des instructions de comparaison SIMD, ajouter de quoi faire la prédication. ===Le calcul des masques et le registre de masque=== La prédication demande que le résultat d'une comparaison soit stocké dans un registre à prédicat ou dans le registre d'état. Pour les processeurs SIMD, il y a cependant une grosse adaptation à faire concernant le fonctionnement des comparaisons et le stockage des résultats. Premièrement, les instructions de comparaison SIMD comparent une paire de deux éléments à la même place dans deux vecteurs et fournissent un résultat d'un bit pour chaque paire. Le résultat est donc un ensemble de bits, qui sont regroupées dans un vecteur spécialisé. Deuxièmement, reste à savoir que faire de ce vecteur. Pour cela, deux solutions sont possibles. Avec la première, le vecteur est stocké dans un registre vectoriel/SIMD comme les autres, il est traité comme n'importe quel autre vecteur. Avec la seconde solution, il reçoit un registre dédié, qui est un mix entre registre d'état et registre à prédicat, appelé un '''registre de masque''' (''Vector Mask Register''). Il stocke un bit pour chaque donnée présente dans le vecteur à traiter, qui indique s'il faut ignorer la donnée ou non. L'instruction SIMD suivante fait usage de ce masque pour savoir s'il faut faire l'opération pour chaque élément du vecteur. [[File:Vector mask register.png|centre|vignette|upright=2|Vector mask register]] Il peut y avoir un ou plusieurs registres de masque. S'il y en a plusieurs, ils sont généralement nommés, sur le modèle des registres à prédicats. L'application d'un masque demande de fournir le nom du registre de masque à utiliser, ils adressés explicitement dans les instructions. S'il n'y en a qu'un seul, il est possible de le pas le nommer et de faire en sorte qu'il soit adressé implicitement par les instructions qui en ont besoin. On parle alors de '''masquage implicite'''. Le masquage implicite est très utilisé sur les cartes graphiques, qui sont actuellement des architectures SIMD, comme nous le verrons plus bas. Un gros problème du masquage implicite est qu'il introduit des dépendances implicites entre instructions, qui ne sont pas explicites quand on regarder uniquement les registres manipulés par une instruction, qui perturbent le renommage de registres. Rien d'insurmontable, mais cela peut poser des problèmes de performance et la résolution de ce problème a un cout en hardware. Mais ce n'est un problème que sur les architectures à exécution dans le désordre. Les architectures à émission dans l'ordre ne sont pas concernées, et c'est notamment le cas sur les GPU et les cartes graphiques modernes. Diverses optimisations sont possible avec un registre de masque. La première est de ne pas exécuter d'instruction si tous les bits du masque sont à 0. Dans ce cas, l'instruction SIMD n'a pas de calculs à faire, vu que tous les résultats sont masqués. Il est alors possible de ne pas exécuter l'instruction SIMD et de passer directement à la suivante. L'implémentation matérielle est très simple : une porte NOR qui détermine si le registre de masque contient la valeur 0. La sortie de cette porte NOR est envoyée au séquenceur d'instruction. Le décodeur est alors conçu pour zapper l'instruction si le registre de masque contient un zéro. ===Les accès mémoire en ''scatter-gather'' avec prédication=== Un problème avec les accès mémoire que certains accès peuvent déclencher des défauts de page ou d'autres formes d'exceptions (erreurs de protection mémoire, autre). Si l'accès mémoire se fait sans prédication, ces exceptions sont gérées dans l'ordre des accès mémoire, en commençant par le début du vecteur, ce qui rend l'implémentation assez facile. Mais avec la prédiction, il se peut qu'un accès masqué et censé être ignoré déclenche une exception matérielle. Et le résultat dépend de l'implémentation. Une première solution est d'effectuer l'accès mémoire, et de masquer ensuite les éléments à ignorer. Mais dans ce cas, les éléments masqués peuvent déclencher des exceptions, qui ne sont pas censées se produire. Une autre solution est de déterminer quelles sont les adresses qu'il faut lire ou écrire et ne pas faire les accès mémoire masqués. Une autre solution effectue tous les accès avant de savoir quels sont ceux à masquer, mais de ne pas déclencher d'exception avant qu'on sache quels sont les accès masqués. Une fois le masquage des accès effectués, le processeur exécute les exceptions adéquates. Il s'agit d'un comportement de masquage d'exception, très utilisé. L'implémentation est assez simple pour les accès mémoire contiguës, mais plus compliqué pour les accès en ''stride'' ou en ''scatter-gather''. Pour les accès contiguës, les seules exceptions pouvant survenir sont celles ayant lieu quand on passe d'une page mémoire à la suivante (au sens pagination, mémoire virtuelle). Et le hardware pour détecter un débordement de page est assez simple. Mais pour les accès en ''stride'' ou en ''scatter-gather'' demandent qu'oin vérifie les exceptions pour chaque élément du vecteur. ===L'implémentation matérielle=== L'implémentation matérielle est assez proche des processeurs SIMD purs. On retrouve plusieurs unités de calcul travaillant en parallèle, avec quelques circuits ajoutés pour gérer la prédication. Le cout supplémentaire en circuits est acceptable, les gains en performance associés sont très intéressants. Les circuits en plus, pour gérer la prédication, sont similaires à ceux utilisés pour la prédication sur les architectures non-SIMD, mais sont dupliqués. La seule innovation architecturale est l'implémentation du registre de masque. Tous les processeurs n'en utilisent pas, mais c'est la solution la plus performante. En effet, ce registre de masque est un peu l'équivalent du registre d'état pour les instructions SIMD, bien qu'il soit techniquement une concaténation de registres à prédicat non-nommés. Ils ont pour avantage de libérer des registres SIMD normaux tout en ayant un cout en matériel très limité. Un autre point est que pour gérer les masques, les instructions SIMD à prédicat doivent lire le masque. Sans registre de masque, en utilisant un registre SIMD normal, il faut rajouter un troisième port de lecture sur le banc de registre pour récupérer le masque, ce qui est couteux en matériel. Pas besoin avec un registre de masque, qui n'est connecté qu'à l'ALU, sur le même modèle que le registre d'état. Plus haut, on a vu que l'implémentation de l'ALU SIMD peut se faire en utilisant des moins d'ALU que prévu. Par exemple, pour des vecteurs de 32 éléments, on peut utiliser 16 ALUs (allant à double fréquence ou non). Une instruction SIMD est alors décodée en plusieurs micro-instructions SIMD consécutives, chacune traitant une partie d'un vecteur. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. L'avantage est que cela se marie bien avec l'abandon des opérations pour les masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit vérifier que les 8 bits de poids faible ainsi que de poids fort du registre de masque ne soient pas individuellement à zéro. Cela se fait en dupliquant la porte NOR évoquer plus haut, l'une pour la partie basse du registre et la seconde pour la partie haute. ==La prédication pour les structures de contrôle imbriquées== Au niveau du jeu d’instruction, les architectures SIMT implémentent de la prédication, sous une forme améliorée. Les processeurs SIMT actuels sont surtout utilisées sur les processeurs intégrés aux cartes graphiques. Et ces derniers gèrent très mal les branchements, et encore : beaucoup de cartes graphiques, même récentes, ne gèrent tout simplement pas les branchements. Elles doivent donc se débrouiller avec uniquement la prédication, là où les processeurs SIMD utilisent des branchements normaux en complément de la prédication. Insistons sur le fait que cet usage exclusif de la prédication n'est présent que sur une sous-partie des architectures SIMT, le seul exemple que l'auteur de ce wikilivre connait étant celui des cartes graphiques. Les architectures SIMT sans branchements doivent donc trouver des solutions pour gérer les structures de contrôle imbriquées, à savoir une boucle placée à l'intérieur d'une autre boucle, un IF...ELSE dans un autre IF...ELSE, etc. Elles utilisent pour cela la prédication, combinée avec des mécanismes annexes. Le premier d'entre eux est l'usage de plusieurs registres de masques organisés d'une manière bien précise, l'autre est l'usage de compteurs d'activité. Voyons ces deux techniques. ===La pile de masques=== La '''pile de masques''' remplace le ou les registres de masque. Sans elle, le processeur SIMD incorpore un registre de masque qui est adressé implicitement ou explicitement. Éventuellement, le processeur peut contenir plusieurs registres de masque séparés adressables via un nom de registre. Avec elle, le processeur SIMD incorpore plusieurs registres de masque organisé en pile. Le registre de masque est donc remplacé par une mémoire LIFO, une pile, dans laquelle plusieurs masques sont empilés. Le tout forme une pile, similaire à la pile d'appel, sauf qu'elle est utilisée pour empiler des masques. Un masque est calculé et empilé à chaque entrée dans une structure de contrôle, puis dépilé une fois la structure de contrôle exécutée. L'empilement et le dépilement des masques est effectué par des instructions PUSH et POP, présentes dans le jeu d'instruction du processeur SIMD. Le calcul des masques doit répondre à plusieurs impératifs. * Premièrement, chaque masque se calcule en faisant un ET entre le masque précédent et le masque calculé par l'instruction de test. Cela permet de ne pas réveiller d’élément au beau milieu d'une structure imbriquée. Si in IF désactive certains éléments du vecteur, une condition imbriquée dans ce IF ne doit pas réveiller cet élément. Le fait de faire un ET entre les masques garantit cela. * Deuxièmement, les masques doivent être empilés et dépilés correctement. Au moment de rentrer dans une structure de contrôle, on effectue une instruction de test associée à la structure de contrôle, qui calcule un masque, et on empile le masque calculé. Au moment de sortir de la structure de contrôle, on dépile le masque en question. L'implémentation demande d'utiliser une mémoire LIFO pour stocker la pile de masques, et quelques circuits annexes. Il faut notamment un circuit relié à l'ALU qui récupère les conditions, les résultats des comparaisons, et qui effectue le ET pour combiner les maques. Pour donner un exemple, prenons le code suivant, qui est volontairement simpliste et ne sert qu'à des fins d'explication : <syntaxhighlight lang="c"> if ( condition 1 ) { if ( condition 2 ) { ... } else { ... } Autres instructions } Instructions après le IF... </syntaxhighlight> Imaginons que l'on traite des vecteurs de 8 éléments. Pour le vecteur considéré, la première condition (a > 0) n'est respectée que par les 4 premiers éléments. L'instruction de condition calcule alors le masque correspondant : 1111 0000. Le masque est alors calculé, puis empilé au somment de la pile. La seconde instruction de test, qui teste la variable b, est maintenant valide pour les 4 bits du milieu du masque. Mais n'allez pas croire que le masque correspondant soit 0011 11100 : il faut tenir compte de la condition précédente, qui a éliminé les 4 derniers éléments. Pour cela, on fait un ET logique entre le masque précédent, et le masque calculé par la condition. Le masque au sommet de la pile est donc lu, combiné avec le masque calculé par l'instruction, ce qui donne le masque final. Le masque final est alors empilé au sommet de la pile. On exécute alors l'instruction du IF, en tenant compte du masque qui est au sommet de la pile. Si le IF était plus compliqué, toutes les instructions suivantes tiendraient compte du masque. En fait, le masque est pris en compte tant qu'il n'est pas dépilé. Une fois que le IF est terminé, le masque est dépilé. On passe alors au ELSE, et rebelotte. Le masque pour le ELSE est calculé en combinant le masque au sommet de la pile avec la condition du ELSE. Le masque au sommet de la pile est celui calculé à l'entrée du premier IF, pas le second qui a été dépilé. Les instructions du ELSE sont alors exécutées en tenant compte de ce masque. Une fois qu'elles sont toutes exécutées, le masque est dépilé. Puis vient l'exécution des instructions après le ELSE. Elles utilisent le masque empilé au sommet de la pile, qui correspond à celui à l'entrée du IF. Puis vient le moment d'exécuter les instructions après le IF : pas de masque, on exécute sur tout le vecteur. ===Les compteurs d'activité=== Une variante de la technique précédente remplace la pile de masques par des '''compteurs d'activité'''. La technique est similaire, si ce n'est qu'elle utilise moins de circuits. Avant , on avait une pile de masques de même taille, dont les bits sont à 0 ou 1 suivant que la condition est remplie. La pile de masque ressemble donc à ceci : {|class="wikitable" |- ! masque 1 | 1 || 1 || 1 || 1 |- ! masque 2 | 0 || 1 || 1 || 1 |- ! masque 3 | 0 || 1 || 1 || 1 |- ! masque 4 | 0 || 0 || 0 || 1 |- ! masque 1 | colspan="4" | vide |} Une manière équivalent de représenter cette pile de masque est de compter combien de bits sont à 0 dans chaque colonne. Attention : j'ai bien dit à 0 ! On obtient alors : {|class="wikitable" |- ! masque 1 | 3 || 1 || 1 || 0 |} Et c'est le principe caché derrière la techniques des compteurs d'activité. Chaque élément dans un vecteur, chaque place, se voit attribuer un compteur. Un compteur non-nul indique qu'il ne faut pas prendre en compte l’élément. Ce n'est qu'une fois que le compteur est nul que l'on effectue des opérations sur l’élément associé du vecteur. À chaque fois qu'on entre dans une structure de contrôle, on teste une condition sur chaque élément. Si la condition est respectée pour un élément, alors le compteur ne change pas. Mais si la condition n'est pas respectée, alors on incrémente le compteur associé. En sortant de la structure de contrôle, on décrémente le compteur associé. Notons que les compteurs qui n'ont pas été incrémenté en entrant dans la structure de contrôle ne sont pas décrémenté en sortant. En clair, là où on empilait/dépilait un masque, on se contente d'incrémenter/décrémenter un compteur. Utiliser un compteur en lieu et place d'une colonne entière dans la pile de masque utilise moins de bits. Et c'est sans doute pour cette raison que certaines cartes graphiques, comme les cartes graphiques intégrées d'Intel depuis 2004, utilisent cette technique. ==Les processeurs vectoriels== Les '''processeurs vectoriels''' sont les ancêtres des processeurs à instruction SIMD. Bien qu'ils soient arrivés en premier, ils incorporent diverses techniques que de simples instructions SIMD n'ont pas forcément. C'est paradoxal, mais c'est ainsi. La première différence se manifeste au niveau du jeu d'instruction. Les processeurs vectoriels sont capables de traiter des vecteurs de taille variable, alors que les instructions SIMD usuelles ne gèrent que des vecteurs de taille fixe. Par taille variable, on veut dire que le processeur gère nativement des vecteurs d'une taille allant de 1 à la taille maximale d'un vecteur. La taille maximale d'un vecteur est celle permise par la taille des registres vectoriels, il s'agit de la taille d'un vecteur SIMD équivalent sur les autres processeurs SIMD. La seconde différence est une différence en termes de micro-architecture. Un processeur SIMD actuel dispose d'une unité de calcul très élaborée, capable de faire plusieurs calculs en parallèle, au moins un pour chaque élément du vecteur. Par exemple, si un vecteur contient 16 entiers, l'ALU SIMD doit contenir au moins 16 additionneurs entiers. Mais sur un processeur vectoriel, ce n'est pas le cas : l'unité de calcul est en réalité une unité de calcul entière/flottante normale, mais qui a la particularité d'être pipelinée. Les calculs sont donc effectués en parallèle, mais d'une manière totalement différente. La plupart de ces différences s'expliquent par le fait que les anciens processeurs vectoriels devaient faire avec des limitations en termes de circuits. Ils avaient peu de transistors à leur disposition, ce qui fait que leur unité de calcul devait être la plus simple possible. D'où l'utilisation d'une unité de calcul simple, mais utilisée de manière pipelinée. De plus, les processeurs vectoriels ne possèdent aucune mémoire cache pour les données et se contentent juste de caches d'instruction. De tels caches sont généralement peu utiles quand on manipule des tableaux, chose quasiment systématique quand on travaille avec du parallélisme de données. ===Une unité de calcul pipelinée=== La différence entre processeur vectoriel et SIMD tient dans la façon dont sont traités les vecteurs : les instructions SIMD traitent chaque élément en parallèle, alors que les processeurs vectoriels pipelinent ces calculs ! Par pipeliner, on veut dire que l’exécution de chaque instruction est découpée en plusieurs étapes indépendantes. Au lieu d'attendre la fin de l’exécution d'une opération avant de passer à la suivante, on peut commencer le traitement d'une nouvelle donnée sans avoir à attendre que l'ancienne soit terminée. [[File:Pipeline.png|centre|vignette|upright=2|Pipeline vectoriel.]] Pour donner un exemple, on peut donner l'exemple d'une multiplication flottante effectuée entre deux registres. Son exécution peut être décomposée en plusieurs étapes. Par exemple, on peut avoir 3 étapes : * une première étape E qui va additionner les exposants et gérer les diverses exceptions ; * une étape M qui va multiplier les mantisses ; * et enfin une étape A qui va arrondir le résultat. L’exécution de notre opération flottante sur un vecteur donnerait donc quelque chose dans le genre, où chaque ligne correspond au traitement d'un nouvel élément dans un vecteur, dans un paquet SIMD. [[File:Multiplication vectorielle - pipeline.png|centre|vignette|upright=2|Multiplication vectorielle - pipeline]] : Certains processeurs vectoriels utilisent plusieurs ALU pipelinées pour accélérer les calculs. On peut les voir comme des intermédiaires entre processeur vectoriels et SIMD SWAR. Avec une unité de calcul pipelinée découpée en N étages, on peut gérer N données simultanées : autant qu'il y a d'étapes différentes. Mais ce nombre maximal de données met un certain temps avant d'être atteint. L'unité de calcul met du temps avant d'arriver à son régime de croisière. Durant ce temps, elle n'a pas commencé à traiter suffisamment d’éléments pour que toutes les étapes soient occupées. Ce temps de démarrage est strictement égal du nombre d'étapes nécessaires pour effectuer une instruction. La même chose arrive vers la fin du vecteur, quand il ne reste plus suffisamment d’éléments à traiter pour remplir toutes les étapes. [[File:Startup and dead time vector pipeline.png|centre|vignette|upright=2|Startup and dead time vector pipeline]] Pour amortir ces temps de démarrage et de fin, certains processeurs démarrent une nouvelle instruction sans attendre la fin de la précédente : deux instructions peuvent se chevaucher pour remplir les vides. Par contre, il peut y avoir des problèmes lorsque deux instructions qui se suivent manipulent le même vecteur. Il faut que la première instruction ait fini de manipuler un élément avant que la suivante ne veuille commencer à le modifier. Pour cela, il faut avoir des vecteurs suffisamment qui contiennent plus d’éléments qu'il n'y a d'étapes pour effectuer notre instruction. ===La technique du ''chaining''=== La technique du pipeline peut encore être améliorée dans certains cas particuliers. L'idée est simplement d'ajouter la technique du contournement (''bypass'') à l'unité de calcul. Pour rappel, le contournement connecte la sortie de l'ALU à une de ses entrées, ce qui permet au résultat d'un calcul d'être réutilisable au cycle d’horloge suivant. L'usage du contournement sur les processeurs vectoriels porte le nom de '''''Vector Chaining'''''. Un processeur implémentant le ''chaining''/''bypass'' a toutes ses unités de calcul reliées entre elles : la sortie d'une unité est reliée aux entrées de toutes les autres. Pour comprendre à quoi peut servir cette technique, on peut citer deux grands exemples principaux. Le premier avantage est que l'implémentation des instructions SIMD horizontales est beaucoup plus simple. C'est pour cela que les processeurs vectoriels intègrent souvent des instructions horizontales, et notamment des instructions arithmétiques horizontales. Le contraste avec les processeurs SIMD récents, qui utilisent plusieurs ALU travaillant en parallèle, est frappant. Ces derniers n’intègrent pas d'instructions horizontales arithmétiques, les seules opérations horizontales supportées sont généralement des permutations ou des instructions ''compress/expand'', guère plus. Maintenant, introduisons une autre possibilité par un exemple. Dans cet exemple, on suppose que le processeur dispose d'une ALU pour l'addition et d'une autre pour la multiplication. Imaginons que l'on ait trois vecteurs nommés A, B et C. Pour chaque énième élément de ces paquets, je souhaite effectuer le calcul <math>A_n + B_n \times C_n</math>. En théorie, il faudrait faire d'abord la multiplication, stocker le résultat temporaire de la multiplication dans un registre vectoriel, puis faire l'addition. Mais en rusant un peu, on peut utiliser le pipeline plus efficacement. Une fois que le premier élément de la multiplication du premier vecteur est connu, pourquoi ne pas démarrer l'addition immédiatement après, et continuer la multiplication en parallèle ? Après tout, les deux calculs ont lieu dans des ALUs séparés. Et le contournement permet d'alimenter l'ALU d'addition avec la sortie du circuit multiplieur. [[File:Vector chaining.png|centre|vignette|upright=2|Vector chaining]] ===La gestion des vecteurs de taille variable=== L'usage d'une unité de calcul pipelinée fait que la taille des vecteurs n'est pas forcément fixe. Autant les processeurs SIMD non-vectoriels disposent d'un nombre fixe d'unités de calcul, et peuvent donc gérer des vecteurs de taille fixe, autant ce n'est pas le cas des processeurs vectoriels. Une unité de calcul pipelinée à 16 étage peut utiliser les trois premiers pour traiter un vecteur de 3 élements, les 5 suivants pour un second vecteur de 5, et le reste pour un troisième vecteur. Reste que la gestion des vecteurs de taille variable doit être gérée au niveau du séquenceur, mais surtout : doit être permise au niveau du jeu d'instruction. Pour cela, il faut ajouter au jeu d'instruction de quoi indiquer la taille du vecteur en cours de traitement. Et c'est le rôle d'un registre spécialisé, le '''Vector Length Register''', que de gérer les vecteurs de taille variable. Le ''Vector Length Register'' est un registre qui indique combien d’éléments on doit traiter dans un vecteur. On peut ainsi dire au processeur : je veux que tu ne traites que les 40 premiers éléments présents d'un paquet. Ils sont utilisés pour gérer des tableaux dont la taille n'est pas un multiple d'un vecteur. Quand on arrive à la fin d'un tableau, il suffit de configurer le ''Vector Length Register'' pour ne traiter que ce qu'il faut. [[File:Vector length register.png|centre|vignette|upright=2|Vector length register.]] Il facilite l'usage du '''déroulage de boucle''', une optimisation qui vise à réduire le nombre d'itérations d'une boucle en dupliquant son corps en plusieurs exemplaires. Le compilateur réplique le corps de la boucle (les instructions à répéter) en plusieurs exemplaires dans cette boucle, avant de corriger le nombre d'itérations de la boucle. Par exemple, prenons cette boucle, écrite dans le langage C : <syntaxhighlight lang="c"> int i; for (i = 0; i < 100; ++i) { a[i] = b[i] * 7 ; } </syntaxhighlight> Celle-ci peut être déroulée comme suit : <syntaxhighlight lang="c"> int i; for (i = 0; i < 100; i+=4) { a[i] = b[i] * 7 ; a[i+1] = b[i+1] * 7 ; a[i+2] = b[i+2] * 7 ; a[i+3] = b[i+3] * 7 ; } </syntaxhighlight> Les instructions vectorielles permettent de traiter plusieurs éléments à la fois, ce qui fait que plusieurs tours de boucles peuvent être rassemblés en une seule instruction SIMD. Le déroulage de boucles permet d'exposer des situations où ce regroupement est possible. Dans notre exemple, si jamais notre processeur dispose d'une instruction de multiplication capable de traiter 4 éléments du tableau a ou b en une seule fois, la boucle déroulée peut être vectorisée assez simplement en utilisant une multiplication vectorielle (que nous noterons vec_mul). <syntaxhighlight lang="c"> int i; for (i = 0; i < 100; i+=4) { vec_a[i] = vec_mul ( vec_b[i] , 7 ) ; } </syntaxhighlight> Le déroulage de boucles n'est toutefois pas une optimisation valable pour toutes les boucles. Reprenons l'exemple de la boucle vue plus haut. Si jamais le tableau à manipuler a un nombre d’éléments qui n'est pas multiple de 4, la boucle ne pourra être vectorisée, vu que la multiplication vectorielle ne peut traiter que 4 éléments à la fois. Pour ce faire, les compilateurs utilisent généralement deux boucles : une qui traite les éléments du tableau avec des instructions SIMD, et une autre qui traite les éléments restants avec des instructions non vectorielles. Cette transformation s'appelle le '''strip-mining'''. Par exemple, si je veux parcourir un tableau de taille fixe contenant 102 éléments, je devrais avoir une boucle comme celle-ci : <syntaxhighlight lang="c"> int i; for (i = 0; i < 100; i+=4) { a[i] = b[i] * 7 ; a[i+1] = b[i+1] * 7 ; a[i+2] = b[i+2] * 7 ; a[i+3] = b[i+3] * 7 ; } for (i = 100; i < 102; ++i) { a[i] = b[i] * 7 ; } </syntaxhighlight> Les processeurs vectoriels utilisent le ''Vector Length Register'' pour éviter d'utiliser le strip-mining. Avec ce registre, il est possible de demander aux instructions vectorielles de ne traiter que les n premiers éléments d'un vecteur : il suffit de placer la valeur n dans le ''Vector Length Register''. Évidemment, n doit être inférieur au nombre d’éléments maximal du vecteur. Avec ce registre, on n'a pas besoin d'une seconde boucle pour traiter les éléments restants, et une simple instruction vectorielle peut suffire. L'avantage principal est que cela réduit la portion de code non-parallélisable, donc augmente l'efficience SIMD. Mais l'intérêt n'est pas qu'une question de code. Il faut aussi prendre en compte que cela permet d'utiliser l'ALU plus efficacement. Dès qu'un vecteur court est terminé, le processeur peut immédiatement démarrer le calcul d'un autre vecteur, le vecteur suivant, l'instruction suivante, sans laisser de cycles inutilisés. Avec un processeur SIMD non-vectoriel, on aurait du utiliser de la prédication, donc utiliser seulement une partie des ALU SIMD, ce qui aurait été un gâchis. ===Les accès mémoire=== Les accès mémoire des processeurs vectoriels sont sensiblement les mêmes que ceux des autres processeurs SIMD. Les instructions d'accès mémoire sont les mêmes, on retrouve les modes d'adressages précédents, dont les accès en ''scatter-gather'' et en ''stride''. Sur certains processeurs vectoriels, les instructions vectorielles lisent et écrivent en mémoire RAM, sans passer par des registres : on parle de '''processeurs vectoriels mémoire-mémoire'''. Elles n'avaient pas d'instructions LOAD et STORE, les instructions arithmétiques géraient directement la lecture des opérandes en mémoire, et incrémentaient automatiquement les adresses à lire/écrire. Elles pouvaient gérer des vecteurs de taille arbitraire, la gestion d'un tableau se faisait parfois en une seule instruction ! Le problème est que l'absence de registres pour stocker les données lues réduisait grandement les performances. [[File:Processeur vectoriel mémoire-mémoire.png|centre|vignette|upright=2|Processeur vectoriel mémoire-mémoire]] Les processeurs vectoriels mémoire-mémoire étaient cependant minoritaires, la grosse majorité des processeurs vectoriels récents possèdent des registres vectoriels dédiés aux vecteurs. La plupart sont des architectures de type LOAD-STORE, mais pas toute. De fait, il n'y a pas de lien strict entre adressage des opérandes et processeurs vectoriel (ou SIMT). Seuls les processeurs SIMD de type SWAR ont une restriction là-dessus et sont systématiquement des architectures LOAD-STORE. [[File:Processeur vectoriel à registres vectoriels.png|centre|vignette|upright=2|Processeur vectoriel à registres vectoriels.]] Il faut noter que l'implémentation des accès mémoire est potentiellement différente de celle des autres processeurs SIMD. En principe, on peut utiliser une implémentation identique entre les deux. Mais le fait que les processeurs vectoriels sont pipelinés fait qu'il est préférable d'utiliser une implémentation différente, qui se base sur une RAM ou des caches pipelinés. Vu que l'ALU reçoit une/deux opérandes par cycle, l'idéal est que la mémoire ou le cache aillent au même rythme et soient donc pipelinés.Bien sur, il est possible d'utiliser une implémentation plus classique, avec des caches multiports, un chargement en plusieurs fois des vecteurs depuis le cache, des circuits pour réorganiser les accès mémoire, etc. En théorie, les architectures vectorielles font peu usage de mémoires cache. Les transferts entre registres vectoriels et mémoire RAM sont directs, sans intermédiaire. La raison est que les applications qui tirent parti du SIMD ont une bonne localité spatiale, mais une mauvaise localité temporelle. Or, les caches de données profitent surtout de la localité temporelle pour donner de bonnes performances. De plus, rappelons que les processeurs vectoriels sont assez anciens et datent d'une époque où les transistors étaient précieux. Il valait mieux utiliser des transistors dans un gros cache d'instruction que pour un cache de donnée peu efficace. ==Les systèmes SIMD à plusieurs processeurs : le SIMT== Enfin, terminons avec le dernier type de SIMD, qui date du tout début de l'informatique : celui où on combine plusieurs processeurs non-SIMD. L'idée est de synchroniser plusieurs processeurs de manière à ce qu'ils exécutent la même instruction en même temps, chacun sur des données différentes. On parle d''''exécution ''lockstep''''' des instructions. Toute la difficulté est de garantir la synchronisation des processeurs, garantir que tous les processeurs exécutent la même instruction. Ce qui pose problème quand des branchements sont impliqués, des processeurs pouvant prendre le branchement et pas les autres. ===Les ''array processors'' des temps anciens=== Les premières architectures matérielles conçues pour le parallélisme de données étaient de ce type. On peut par exemple citer l'exemple des Thinking machines CM-1 et CM-2, qui exécutaient tous la même instruction au même cycle d'horloge sur 64000 processeurs minimalistes. Un autre exemple est celui de l'ILLIAC IV. Historiquement, le terme utilisé pour désigner de telles architectures était '''array processors''', encore que ce terme fût réservé aux systèmes multiprocesseurs. Par la suite, le terme ''Single Instruction Multiple Threads'', ou SIMT, a été utilisé. Mais ce terme a ensuite été repris par le marketing de NVIDIA pour désigner une technique précise de SIMD avec prédication, utilisée sur ses cartes graphiques récentes. Le mauvais usage de la terminologie fait que le terme SIMT est devenu polysémique et quelque peu trompeur. L'ILLIAC 4 était un ordinateur un peu particulier, presque unique en son genre. Son architecture était composé d'un processeur principal couplé à plusieurs processeurs secondaires. Il y avait en tout un processeur principal et 64 processeurs secondaires. Le processeur principal chargeait les instructions depuis la mémoire, les décodaient, puis envoyait les instructions aux processeurs secondaires. Le processeur principal était le seul à avoir un ''program counter'', il était utilisé pour gérer les boucles, ainsi que pour configurer les instructions à prédication. Cependant, le processeur principal n'était pas un simple séquenceur : il pouvait exécuter des instructions, avait un accumulateur et des registres, etc. Il prenait en charge toute instruction non-SIMD, alors que les instructions SIMD étaient envoyées aux processeurs secondaires. Les 64 processeurs secondaires avaient chacun : une unité de calcul, un ''local store'' de 2048 flottants de 64 bits, une unité mémoire. Ils n'ont pas de séquenceur ni de ''program counter'', ni de quoi charger une instruction. L'unité de calcul est appelée un ''processing element'' ou PE. Elle intègre un additionneur flottant, de quoi faire des décalages, et une unité de calcul logique. De plus, un PE intègre des registres accumulateurs et de quoi calculer des adresses : un registre d'indice, un registre d'adresse, un circuit de calcul d'adresse. L'unité mémoire connectait l'ALU entière PE et le ''local store'', et elle permettait aussi d'adresser des entrées-sorties. Pour gérer la prédication, les processeurs secondaires pouvaient être activés ou désactivés suivant le résultat des branchements/conditions. Intuitivement, on peut faire le rapprochement avec un processeur SIMD. Le processeur principal regrouperait la partie non-SIMD d'un processeur SIMD, alors que les processeurs secondaires seraient l'unité de calcul SIMD. Cependant, il y a une différence très importante : il n'y a pas de banc de registre vectoriel. À la place, chaque processeur secondaire a son propre banc de registre, et ce sont des registres scalaires ! Une autre différence est que la mémoire RAM est découpée en plusieurs ''local store'' de 2048 adresses, chacun étant relié à une unité de calcul PE. N'oublions pas le système de calcul d'adresse et l'unité LOAD/STORE qui est fusionnée avec l'unité SIMD, chaque PE ayant ses circuits de calcul d'adresse, sa propre unité mémoire, etc. ===Les anciennes cartes graphiques hybrides SIMD-VLIW=== Une technique de SIMD multi-processeur a été utilisée autrefois sur certaines cartes graphiques AMD à architecture Terascale. La différence est que l'architecture en question était multicœur et non multi-processeurs. Elles contenaient plusieurs cœurs VLIW, qui fonctionnaient en ''lockstep''. Un groupe de 16 processeurs VLIWs exécutaient la même opération VLIW. Du moins, c'est ce que semblent dire les [https://www.techpowerup.com/gpu-specs/docs/amd-gcn1-architecture.pdf white papers de présentation de l'architecture GCN]. L'usage de cet hybride SIMD-VLIW était très adapté au traitement graphique. Les cartes graphiques post-années 2000 sont capables d'exécuter des programmes appelés des '''''shaders''''', qui sont utilisés en rendu 3D, pour calculer certaines scènes 3D, et notamment pour calculer les ombres (d'où leur nom de shader, pour shade - ombre). Or, ces shaders sont justement exécutés par la carte graphique, sur les processeurs de shaders. Un processeur de ''shader'' applique un programme/''shader'' sur chaque ''pixel'' ou triangle d'une scène 3D (en fait des fragments et des vertices, mais faisons cette simplification). Chaque triangle ou pixel d'une scène 3D peut être traité indépendamment des autres, ce qui rend le traitement 3D fortement parallèle. Aussi, l'usage du SIMD est assez naturel : chaque processeur exécute un ''shader'' sur un pixel/triangle, utiliser une architecture SIMD parait naturel. Reste à justifier l'usage de plusieurs CPU VLIW. La raison tient dans la manière dont est traité un pixel par un shader. Un pixel est codé en utilisant quatre informations. Premièrement, une couleur codée avec trois nombres flottants, qui représentent une couleur au format RGB avec un mélange de rouge, vert et bleu, chaque couleur rouge/bleu/vert étant codée avec un flottant de 32 bits. La quatrième information est un nombre qui code la transparence du pixel. Généralement, le shader traite différemment la transparence de la couleur RGB, dans le sens où les instructions utilisées ne sont pas les mêmes. Au lieu de traiter couleur RGB et transparence séparément, elles sont traitées en même temps en parallèle, ce qui demande de pouvoir émettre deux instructions : une pour le calcul de la transparence, une autre pour la couleur RGB. Fait amusant, l'instruction utilisée pour gérer la composante RGB est une instruction vectorielle qui travaille sur des vecteurs de 3 couleurs. Les architectures VLIW sont parfaites pour cela. Les cartes graphiques de l'époque de DirectX 9 utilisaient cette méthode qui mélangeait SIMD et VLIW. Elles disposaient de plusieurs cœurs VLIW qui exécutaient le même shader en exécution ''lockstep''. Chaque cœur traitait au moins un pixel à la fois, ou un triangle à la fois. Cependant, nous devons préciser que les cœurs VLIW partagent la même unité de contrôle, ce sont en réalité des chemins de données plus qu'autre chose. L'architecture était aussi combinée avec du ''Fine Grained Multithreading'' afin de pouvoir gérer les accès mémoire, de telles architectures ne pouvant pas se permettre d'utiliser l'exécution dans le désordre. De telles architectures multicœurs à exécution ''lockstep'' permettent quelques optimisations, comme le fait de partager le cache d'instruction entre les cœurs. ==Résumé des différents types de processeurs SIMD== Dans ce chapitre, nous avons appris qu'il existe plusieurs catégories de processeurs SIMD. Les différences entre ces catégories tiennent à la fois dans le jeu d'instruction, mais aussi dans la microarchitecture des processeurs en question. Au niveau du jeu d'instruction, il y a trois paliers, qui sont résumés dans le tableau ci-dessous. {|class="wikitable" |- ! ! Support de la prédication ! Taille des vecteurs ! Instructions horizontales ! Architecture LOAD-STORE ! Pointeur de pile |- ! Processeurs SIMD purs | Non | rowspan="3" | Fixe | rowspan="3" | Limitées à des échanges de données à l'intérieur d'un vecteur, pas d'instructions de réduction | Oui | rowspan="2" | Unique |- ! Processeurs SIMD avec prédication | Oui, par définition | rowspan="3" | Dépend du processeur |- ! Processeurs SIMT | rowspan="2" | Oui, sauf exceptions | Un pointeur de pile possible par élément du vecteur |- ! Processeurs vectoriels | Variable | Toutes supportées, y compris les instructions arithmétiques horizontales | Unique |} Au niveau de la microarchitecture, il y a trois paliers, qui sont résumés dans le tableau ci-dessous. {|class="wikitable" |- ! ! Unité de calcul ! Banc de registre ! Adressage des opérandes |- ! Processeurs SIMD | rowspan="2" | Plusieurs ALU simples non-pipelinée, plusieurs additionneurs/multiplieurs/autres | Banc de registres vectoriels | Architecture LOAD-STORE |- ! Processeurs SIMT | Plusieurs bancs de registres scalaires (non-vectoriels, registres normaux) | rowspan="2" | Dépend du processeur |- ! Processeurs vectoriels | ALU simple unique, pipelinée | Banc de registres vectoriels |} Les processeurs SIMD purs ont des performances différentes des processeurs vectoriels, vu que les premiers font leurs calculs en parallèle, alors que les CPU vectoriels utilisent un pipeline. Mais dans les grandes lignes, les performances sont similaires. Les différences sont grandement atténuées par l'usage du ''vector chaining'', de la gestion des vecteurs de taille variables, et des autres optimisations spécifiques aux processeurs vectoriels. L'avantage est cependant du côté des processeurs SIMD pur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Architectures multithreadées et Hyperthreading | prevText=Architectures multithreadées et Hyperthreading | next=La cohérence des caches | nextText=La cohérence des caches }} {{autocat}} </noinclude> 4oaq2mguj0njoil95u8j1hpbbx718hj 773894 773846 2026-10-01T19:16:07Z Mewtow 31375 /* L'implémentation matérielle */ 773894 wikitext text/x-wiki Nous allons maintenant aborder le parallélisme de données, qui consiste à traiter des données différentes en parallèle. De nombreuses situations s'y prêtent relativement bien : traitement d'image, manipulation de sons, vidéo, rendu 3d, etc. Mais pour exploiter ce parallélisme, il a fallu concevoir des processeurs adaptés. Les architectures les plus simples exécutent une instruction sur plusieurs données en parallèle, à l'intérieur d'un processeur. Ces architectures exploitent le parallélisme de données au niveau de l'unité de calcul, celle-ci pouvant exécuter un même calcul sur des données différentes en parallèle. Si on omet quelques exceptions, on peut classer ces architectures dans plusieurs catégories principales : les processeurs à instructions SIMD, les processeurs à instructions SIMD à prédicat, les processeurs SIMT, les processeurs vectoriels. Si les processeurs vectoriels sont assez rares de nos jours, tous les processeurs récents incorporent des instructions SIMD. Quant au SIMT, il est utilisé sur les cartes graphiques modernes. ==Les instructions SIMD : les points communs entre tous les processeurs SIMD== Les processeurs modernes fournissent presque tous des '''instructions SIMD''', qui sont capables de traiter plusieurs éléments en parallèle. Elles travaillent sur des nombres entiers ou flottants regroupés dans ce qu'on appelle un ''vecteur'', qui sont eux-mêmes stockés dans des registres spécialisés. En général, tous les vecteurs ont une taille fixe, peu importe leur contenu. Cela implique que suivant la taille des données à manipuler, on pourra en placer plus ou moins dans un vecteur. Par exemple, un vecteur de 128 bits pourra contenir 4 entiers de 32 bits, 4 flottants 32 bits, ou 8 entiers de 16 bits. [[File:Vector register.png|centre|vignette|upright=2|Contenu d'un vecteur en fonction du type de données utilisé.]] Les vecteurs sont stockés dans des '''registres vectoriels''', aussi appelés '''registres SIMD'''. Un registre vectoriel peut contenir un vecteur complet, pas plus. En conséquence, ils ont une taille assez importante : ils font généralement 128,,256, voire 512 bits, comparé aux 32/64 bits des registres scalaires. Le résultat est que les processeurs SIMD modernes ont des registres SIMD séparés des registres entiers/flottants normaux. Un défaut de cette organisation est que les registres vectoriels sont des registres en plus, qui doivent être sauvegardés lors des commutations de contexte, lors des interruptions, lors des appels systèmes, etc. {| |+ Comparaison entre un processeur sans registres vectoriels, et avec registres vectoriels. |[[File:Non-SIMD cpu diagram1.svg|vignette|upright=1.5|CPU Non-SIMD]] |[[File:SIMD cpu diagram1.svg|vignette|upright=1.5|CPU SIMD]] |} Les instructions SIMD peuvent être rassemblées en deux grands groupes : les horizontales et les verticales. Les '''instructions SIMD verticales''' travaillent en parallèle sur les éléments qui sont "à la même place" dans deux vecteurs. Elles peuvent additionner ou multiplier deux vecteurs, par exemple. Pour prendre l'exemple d'une instruction d'addition vectorielle, celle-ci va additionner ensemble les données qui sont à la même place dans deux vecteurs, et placer le résultat dans un autre vecteur, à la même place. Les '''instructions SIMD horizontales''' partent d'un vecteur et ont pour résultat un simple nombre. Elles peuvent calculer la somme ou le produit des éléments d'un vecteur, renvoyer le nombre d’éléments nuls dans un vecteur, etc. [[File:Instructions SIMD.png|centre|vignette|upright=2.5|Instructions SIMD]] Les instructions SIMD sont difficilement utilisables dans des langages de haut niveau et c'est donc au compilateur de traduire un programme avec des instructions vectorielles. Les transformations qui permettent de traduire des morceaux de programmes en instructions vectorielles (déroulage de boucles, strip-mining) portent le nom de vectorisation. ===Les instructions SIMD arithmétiques verticales=== Les instructions SIMD sont essentiellement des instructions arithmétiques, comme des additions, des soustractions, des multiplications, éventuellement des opérations bit à bit, et quelques autres. Suivant la taille des données, et le type de celle-si, on devra effecteur des instructions différentes. Par exemple, on devra utiliser deux instructions d'addition différentes suivant qu'on manipule des flottants 64 bits ou des entiers 32 bits. De même, l'instruction pour additionner des vecteurs d'entiers de 16 bits sera différente de celle manipulant des vecteurs d'entiers de 32 bits. L'addition et la multiplication peuvent générer des débordements d'entier. Pour les instructions non-SIMD, le débordement d'une addition est géré par un bit de retenue, stocké dans le registre d'état. Mais pour les instructions SIMD, ce n'est pas une solution très facile à implémenter. À la place, les additions et soustractions utilisent généralement l'arithmétique saturée, à savoir que lorsqu'une addition déborde, le résultat est la valeur maximale représentable dans un entier. Pour la multiplication, les instructions non-SIMD génère un résultat qui est codé sur le double du nombre de bits. Une multiplication de deux nombres 64 bits donnera un résultat sur 128 bits, par exemple. Pour gérer cela, les multiplications non-SIMD stockent ce résultat dans deux registres, ou ne conservent que les 64 bits de poids faible. D'autres solutions sont possibles, mais ces deux là sont les plus utilisées. Pour les instructions SMID, une autre solution est préférée car plus simple pour de telles instructions : fournir deux instructions : une qui calcule les 64 bits de poids faible du résultat, une autre pour les 64 bits de poids fort. Les processeurs SIMD étant utilisés pour du traitement d'image ou du rendu 2D/3D, ils supportent généralement des opérations mathématiques assez complexes sur des nombres flottants. Il n'est pas rare d'avoir des instructions SIMD pour le calcul de l'inverse d'un nombre, de sa racine carrée, l'inverse d'une racine carrée, des fonctions trigonométriques, etc. L'implémentation matérielle de telles instructions est généralement très complexe et gourmande en circuits, ce qui fait que les concepteurs de processeurs rusent. Sauf sur certains processeurs assez peu fréquents, les instructions mathématiques complexes ne calculent pas un résultat exact, mais une approximation du résultat. Utiliser un résultat approximatif ne pose pas de problème pour de telles applications, le rendu d'image ou 3D s'en accommodant parfaitement, contrairement au calcul scientifique. ===Les instructions SIMD de manipulation de données intra-vecteur=== Après avoir vu les instructions SIMD verticales, il est temps de voir les instructions SIMD horizontales. Pour rappel, celles-ci travaillent un vecteur unique, dont elles réarrangent ou modifient les éléments. Tout cela sera plus parlant une fois qu'on aura donné quelques exemples. Mais avant toute chose, nous allons séparer les instructions horizontales en deux sous-types, fondamentalement différents. Le premier sous-type change de place les données dans un vecteur. Le second type est celui des instructions horizontales arithmétiques/logiques habituelles, qu'on verra plus tard. La distinction entre les deux est très importante, comme on le verra plus tard. [[File:SIMD instruction data movement - One operand.png|vignette|upright=1.5|Instructions SIMD de mouvement de données à une opérande.]] Les instructions SIMD horizontales les plus communes bougent des données à l'intérieur des vecteurs, ce qui leur vaut le nom d''''instructions de manipulation de vecteur'''. Les plus simples sont celles qui ont une seule opérande, à savoir qui prennent un vecteur comme opérande et renvoient un vecteur résultat. Les plus intuitives sont les instructions de '''permutation''', dont le nom est assez explicite. Elles changent de place des données dans un vecteur. Dans le cas le plus compliqué, elles prennent deux vecteur : le vecteur dans lequel faire la permutation, et un vecteur qui indique comment faire la permutation. Le vecteur précise, pour chaque élement du vecteur, où il doit aller après la permutation. L'instruction effectue la permutation des entiers/flottants/octets dans le vecteur. Les instructions '''''compress''''' et '''''expand''''' sont elles aussi spécifiques aux vecteurs. L'instruction ''compress'' prend un vecteur en opérande, sélectionne certaines valeurs présentes dedans, et les stocke dans un vecteur de sortie. Elle regroupe les valeurs sélectionnées dans le début du vecteur, et laisse les cases inoccupées à 0. Par exemple, pour un vecteur de 16 entiers, elle permet de sélectionner 5 entiers dedans, et les place dans un vecteur résultat qui ne contient que ces 5 valeurs au début du vecteur, le reste est à 0. L'instruction ''expand'' fait l'inverse : elle prend un vecteur crée par l'instruction ''compress'' (ou du moin qui a le même format), prend toutes les valeurs non-nulles dedans, et les disperse dans un vecteur de sortie, en les mettant à la place désirée. Les autres instructions SIMD horizontales prennent deux opérandes, voire aucune opérande vectorielle (elles prennent un nombre, pas un vecteur). Les instructions '''''shuffle''''' prennent plusieurs vecteurs, sélectionnent les éléments adéquats dans chaque vecteur, et les regroupe dans un vecteur résultat. Par exemple, prenons 2 vecteurs de 16 entiers chacun. Une instruction ''shuffle'' peut prendre 8 entiers dans chaque vecteur et les regrouper dans un vecteur résultat unique de 16 entiers. Ou encore, elle peut prendre trois vecteurs de 16 flottants chacun, prendre 4 flottants dans le premier, 10 dans le second et 2 dans le troisième, et regrouper le tout dans un vecteur résultat de 16 flottants. Il arrive que les deux cas précédents correspondent à des cas séparés, à deux instructions de type ''shuffle'', mais qui fonctionnent différemment. Le premier type prend les N premiers éléments du premier vecteur, puis prend les élements manquant à partir de la fin du vecteur. Le second type prend un élément sur deux dans chaque vecteur, mais avec un décalage d'un rang entre les deux vecteurs. Il y a aussi les instructions dites de '''''broadcast''''', qui copient une valeur unique dans un vecteur entier. Elles sont surtout utilisées pour initialiser des tableaux avec une valeur unique, ou pour remplir un vecteur avec des données prédéterminées pour certains calculs impliquant des constantes. [[File:SIMD instruction exemple.png|centre|vignette|upright=1.5|Instructions SIMD de mouvement de données qui ne sont pas à une opérande.]] ===Les instructions SIMD de réduction=== Il existe des instructions horizontales de type arithmétiques. La plus simple est celle qui additionne les différents entiers/flottants d'un vecteur. Elle est utilisée pour accélérer une opération précise : faire la somme d'un tableau d'entier/flottants. On peut aussi citer les opérations maximum et minimum, qui renvoient le plus grand/petit élément d'un vecteur. Un point important avec ces opérations est qu'elles prennent un vecteur, mais renvoie un résultat unique, un scalaire, un nombre entier/flottant seul. Elles sont parfois appelées des '''instructions de réduction'''. Les instructions de réduction sont généralement la spécificité des processeurs vectoriels, les processeurs SIMD récents n'en ont pas. La raison est que leur implémentation matérielle est généralement assez compliquée. Contrairement aux instructions verticales, elles ont une forme de parallélisme de donnée très limitée. Elles ne travaillent pas exactement sur des données indépendantes, elles font des calculs dont le résultat est utilisé par d'autres, il y a des dépendances de données entre éléments du vecteur. Cependant, il faut savoir que l'on peut émuler une instruction de réduction en utilisant des instructions verticales couplées à des instructions de manipulation de vecteur. Par exemples, prenons le cas d'une instruction de réduction qui additionne entre eux les éléments d'un vecteur. On peut l'émuler en utilisant une addition verticale entre deux vecteurs, et une instruction ''compress''. L'idée est la suivante : on coupe le vecteur initial en deux avec deux instructions ''compress'', ce qui donne deux sous-vecteurs. Puis, on additionne les deux vecteurs résultat entre eux. Puis on répète les deux étapes précédentes jusqu'à obtenir la somme finale. Le fait que l'on puisse émuler les instructions de réduction est la raison principale qui explique que les processeurs SIMD récents n’intègrent pas d'instructions de réduction. Ils se débrouillent avec des instructions SIMD de manipulation de vecteur, et des instructions SIMD verticales. C'est un bon compromis entre cout en circuits et performance. ===Les accès mémoire=== La gestion des accès mémoire est assez hétéroclite, la façon de faire étant différente selon les architectures. Dans le cas le plus simple, les données d'un vecteur sont contigües en mémoire et les instructions ont juste à préciser l'adresse mémoire du début du vecteur, qui est généralement dans un registre d'adresse spécialisé, ou un registre non-SIMD. Avec des vecteurs de 8 octets, toute instruction d'accès mémoire de ce type va lire ou écrire des blocs de 8 octets. L'adresse de départ de ces blocs est soumise à des contraintes d'alignement sur les jeux d'instructions comme le SSE, le MMX, etc. La raison à cela est que gérer des accès non alignés en mémoire rend les circuits de lecture/écriture en mémoire plus complexes. En contrepartie, ces contraintes compliquent l'utilisation des instructions SIMD par le compilateur. D'autres modes d'adressage des vecteurs permettent à une instruction de charger des données dispersées en mémoire pour les rassembler dans un vecteur. On peut notamment citer l'existence d'accès mémoires en stride et en scatter-gather. L''''accès en stride''' regroupe des données séparées par un intervalle régulier d'adresses. Ce mode d'accès a besoin de l'adresse initiale, de celle du premier élément du vecteur, et de la distance entre deux données en mémoire. Il permet aux instructions de mieux gérer les tableaux de structures, ainsi que les tableaux multi-dimensionnels. Lorsqu'on utilise de tels tableaux, il arrive assez souvent que l'on n'accède qu'à des éléments tous séparés par une même distance. Par exemple, si on fait des calculs de géométrie dans l'espace, on peut très bien ne vouloir traiter que les coordonnées sur l'axe des x, sans accès sur l'axe des y ou des z. Les instructions d'accès mémoire en enjambées gèrent de tels cas efficacement. Les processeurs SIMD incorporent aussi les accès en '''''scatter-gather'''''. De tels accès prennent en opérande un vecteur contenant des adresses mémoires, et lit/écrit chacune de ses adresses mémoire indépendamment. Les accès en ''scatter-Gather'' peuvent être vus comme une généralisation de l'adressage indirect à registre aux vecteurs, chaque élément du vecteur étant adressé via adressage indirect. Les accès en ''gather'' sont des lectures : elles prennent un vecteur d'adresse, lisent chaque adresse, et regroupent toutes les données lues dans un vecteur, dans un registre vectoriel. Les accès en ''scatter'' sont l'équivalent pour les écritures. Ils prennent un vecteur d'opérande pour les données à écrire, un vecteur pour les adresses. [[File:Cuda4.png|centre|vignette|upright=2|Adressage en scatter-gather]] Un autre version des accès en ''scatter-gather'' n'utilise pas des adresses mémoire, amis des indices. Le vecteur opérande ne contient pas des adresses, mais des indices qui sont combinés à une adresse de base pour calculer plusieurs adresses. Un tel mode d'adressage permet de faciliter l'implémentation de certaines structures de données, appelées des vecteurs de Liffe. Ils sont très utilisés pour gérer les matrices creuses, des matrices où une grande partie des éléments sont nuls et où, dans un souci d'optimisation, seuls les éléments non nuls de la matrice sont stockés en mémoire. Lors d'une instruction de ''scatter'' ou de ''gather'', tous les accès mémoire n'ont pas lieu en même temps, certains accès mémoire seront terminés avant les autres. Les accès peuvent se faire dans le désordre et de nombreuses optimisations matérielle en profitent pour gagner en performance. Il existe cependant un cas où les accès mémoire doivent se faire dans un ordre bien précis, généralement du premier élément vers le dernier : lorsqu'une instruction ''scatter'' effectue plusieurs écritures à la même adresse. C'est parfaitement possible, et c'est le seul cas où il est intéressant de forcer un ordre pour les écritures. En théorie, une instruction SIMD est un tout, c'est à dire qu'elle ne doit pas modifier les registres SIMD avant que tous les accès mémoire se soient terminés. Cependant, il est possible qu'une partie des accès mémoire déclenchent des défauts de page ou lèvent des exceptions matérielles, alors que les autres se sont terminés. En théorie, le processeur est censé se débarrasser du résultat des accès mémoire terminés. Mais ce serait un gachis de performance, ce qui fait que le processeur viole la règle voulant que les registres SIMD soient mis à jour en une seule fois à la fin de l'instruction de ''scatter-gather''. Dès qu'un accès mémoire se termine, il écrit son résultat dans le registre SIMD, à l'endroit adéquat. Cependant, ce comportement pose un problème. Lorsqu'une exception survient, comme lors d'un défaut de page, le processeur exécute la routine d'interruption associée, puis redémarre l'instruction fautive, le second essai étant le bon. Ici, le processeur ne réexecute pas l'instruction à l'identique, mais seulement les accès non-terminés. Mais comment le processeur sait-il quelles accès mémoire redémarrer ? Il doit pour cela mémoriser quels sont les accès mémoire terminés. Il utilise pour cela un registre architectural appelé le '''masque de complétion'''. Le registre est forcément architectural car il doit être préservé lors d'un changement de contexte, l’exécution de l'exception matérielle forçant un changement de contexte. ===Exemple avec les processeurs x86=== [[File:PD-20060908-SSE3-01.svg|vignette|Chronologie des extensions x86 SIMD.]] Après avoir vu la théorie sur les instructions SIMD, il est temps de voir un exemple concret : celui des instructions SIMD des processeurs x86, présents dans nos PC. Le jeu d'instruction des PC qui fonctionnent sous Windows est appelé le x86. C'est un jeu d'instructions particulièrement ancien, apparu en 1978. Depuis, des '''extensions x86''' ont ajouté des instructions au x86 de base. On peut citer par exemple les extensions MMX, SSE, SSE2, ''3dnow!'', etc. Les premières instructions SIMD furent fournies par une extension x86 du nom de MMX, introduite par Intel en 1996 sur le processeur Pentium MMX. Le MMX a perduré durant quelques années avant d'être remplacé par les extensions SSE. La même année, AMD sorti d'expansion ''3DNow!'', qui ajoutait 21 instructions SIMD similaires à celles du MMX. Celui-ci fût suivi du ''3DNow!+'' quelques années plus tard. Le SSE fût ensuite décliné en plusieurs versions avant d'être "remplacé" par l'AVX. Une grande quantité de ces extensions x86 sont des ajouts d'instructions SIMD. L’'''extension MMX''' ajoutait pas mal d'instructions SIMD assez basiques, essentiellement des instructions arithmétiques : addition, soustraction, opérations logiques, décalages, rotations, mise à zéro d'un registre, etc. La multiplication est aussi supportée, mais avec quelques petites subtilités, via l'instruction PMULLW. Les instructions MMX ne mettent pas le registre d'état à jour et ne préviennent pas en cas d'overflow ou d'underflow si ceux-ci arrivent (pour les instructions qui ne travaillent pas en arithmétique saturée). Le MMX introduisait 8 registres vectoriels, du nom de MM0, MM1, MM2, MM3, MM4, MM5, MM6 et MM7. Ils ne pouvaient contenir que des nombres entiers et faisaient 64 bits. Ils avaient cependant un léger défaut, qui a nui à l'adoption du MMX. Pour rappel, avant le MMX, les flottants étaient gérés par l'extension x87, qui définit 8 registres flottants de 80 bits. Et chaque registre MMX correspondait aux 64 bits de poids faible d'un registre flottant x87 ! Le système d'''alias'' de registres typique des CPU x86 a encore frappé ! En conséquence, il était impossible d'utiliser en même temps l'unité de calcul flottante et l'unité MMX. Par contre, sauvegarder les registres lors d'un changement de contexte, d'une interruption, ou d'un appel de fonction était très simple : la sauvegarde des registres de la FPU x87 suffisait. [[File:MMX-FPU-Register.JPG|centre|vignette|upright=2|Registres MMX et FPU x87.]] [[File:XMM registers.svg|droite|vignette|XMM registers]] Dans les années 1999, une nouvelle extension SIMD fit son apparition sur les processeurs Intel Pentium 3 : le '''Streaming SIMD Extensions''', abrévié SSE. Ce SSE fut ensuite complété, et différentes versions virent le jour : le SSE2, SSE3, SSE4, etc. Cette extension fit apparaitre 8 nouveaux registres, les registres XMM. Sur les processeurs 64 bits, ces registres sont doublés et on en trouve donc 16. En plus de ces registres, on trouve aussi un registre d'état qui permet de contrôler le comportement des instructions SSE : celui contient des bits qui permettront de dire au processeur que les instructions doivent arrondir leurs calculs d'une certaine façon, etc. Ce registre n'est autre que le registre MXCSR. Chose étrange, seuls les 16 premiers bits de ce registre ont une utilité : les concepteurs du SSE ont surement préférés laisser un peu de marge au cas où. La première version du SSE contenait assez peu d'instructions : seulement 70. Le SSE première version ne fournissait que des instructions pouvant manipuler des paquets contenant 4 nombres flottants de 32 bits (simple précision). Je ne vais pas toutes les lister, mais je peux quand-même dire qu'on trouve des instructions arithmétiques de base, avec pas mal d'opérations en plus : permutations, opérations arithmétiques complexes, autres. Petit détail : la multiplication est gérée plus simplement et l'on a pas besoin de s’embêter à faire mumuse avec plusieurs instructions différentes pour faire une simple multiplication comme avec le MMX. On peut quand même signaler une chose : des instructions permettant de contrôler le cache firent leur apparition. On retrouve ainsi des instructions qui permettent d'écrire ou de lire le contenu d'un registre XMM en mémoire sans le copier dans le cache. Ces instructions permettent ainsi de garder le cache propre en évitant de copier inutilement des données dedans. On peut citer par exemple, les instructions MOVNTQ et MOVNTPS du SSE premiére version. On trouve aussi des instructions permettant de charger le contenu d'une portion de mémoire dans le cache, ce qui permet de contrôler son contenu. De telles instructions de prefetch permettent ainsi de charger à l'avance une donnée dont on aura besoin, permettant de supprimer pas mal de cache miss. Le SSE fournissait notamment les instructions PREFETCH0, PREFETCH1, PREFETCH2 et PREFETCHNTA. Autant vous dire qu'utiliser ces instructions peut donner lieu à de sacrés gains si on s'y prend correctement ! Il faut tout de même noter que le SSE n'est pas seul "jeu d'instruction" incorporant des instructions de contrôle du cache : certains jeux d'instruction POWER PC (je pense à l'Altivec) ont aussi cette particularité. Avec le '''SSE2''', de nouvelles instructions furent ajoutés, permettant d'utiliser des nombres de 64, 16 et 8 bits dans chaque vecteur. Le SSE2 incorporait ainsi pas moins de 144 instructions différentes. Ce qui commençait à faire beaucoup. Puis, vient le '''SSE3''', avec ses 13 instructions supplémentaires. Pas grand-chose à signaler, si ce n'est que des instructions permettant d'additionner ou de soustraire tous les éléments d'un paquet SSE ensemble, des instructions pour les nombres complexes, et plus intéressant : les deux instructions MWAIT et MONITOR qui permettent de paralléliser plus facilement des programmes. Le '''SSE4''' fut un peu plus complexe et fut décliné lui-même en 2 versions. Le SSE4.1 introduit ainsi des opérations de calcul de moyenne, de copie conditionnelle de registre (un registre est copié dans un autre si le résultat d'une opération de comparaison précédente est vrai), de calcul de produits scalaire, de calcul du minimum ou du maximum de deux entiers, des calculs d'arrondis, et quelques autres. Avec le SSE4.2, le vice à été poussé jusqu'à incorporer des instructions de traitement de chaines de caractères. [[File:AVX registers.svg|vignette|Registres AVX.]] Avec l''''AVX (Advanced Vector eXtensions)''', on retrouve 16 registres d'une taille de 256 bits, nommés de YMM0 à YMM15 et dédiés aux instructions AVX. Ils sont partagés avec les registres XMM : les 128 bits de poids faible des registres YMM ne sont autres que les registres XMM. L'AVX complète le SSE et ses extensions, en rajoutant quelques instructions, et surtout en permettant de traiter des données de 256 bits. Son principal atout face au SSE est que les instructions AVX permettent de préciser le registre de destination en plus des registres d'opérandes. Avec le SSE et le MMX, le résultat d'une instruction SIMD était écrit dans un des deux registres d'opérande manipulé par l'instruction. Il fallait sauvegarder son contenu si on en avait besoin plus tard, ce qui n'est plus nécessaire avec l'AVX. ==La performance des processeurs SIMD== Il est intéressant de comparer la performance d'un processeur SIMD et celle d'un processeur normal, sans SIMD. Mais cet exercice est compliqué par le fait que les processeurs non-SIMD sont très nombreux : entre les CPU superscalaires, à émission dans l'ordre, exécution dans le désordre, ceux sans rien de tout cela, on a de quoi être perdu. Dans ce qui suit, nous allons comparer deux processeurs basiques sans pipeline : un avec SIMD, un autre sans. Comparé à un processeur sans pipeline, on s'attend à ce que la performance soit augmentée d'un facteur N, avec N le nombre maximal d'entiers/flottants que l'on peut mettre dans un vecteur. Si une architecture SIMD fait N calculs en parallèles, alors les performances sont censées être multipliées par N. Cela laisse penser que plus les vecteurs sont longs, plus le gain en performance est important. Il s'agit là d'un résultat basique, mais la vraie vie est différente. Un premier problème est que ce résultat ne tient que si la mémoire RAM et les caches suivent. En effet, faire N calculs en parallèle demande de lire/écrire N fois plus de données. Les données sont souvent stockées dans des registres vectoriels, ce qui fait que la pression sur le banc de registre est assez importante. Mais cela signifie aussi que les instructions d'accès mémoire doivent lire/écrire N fois plus de données. Il faut alors ajouter des ports sur le cache, élargir le bus mémoire, augmenter la taille des lignes de cache, etc. Le débit binaire des caches et de la mémoire RAM deviennent rapidement des points limitants, qui réduisent le gain en performance des instructions SIMD. Et ne parlons pas de l'interaction avec la mémoire virtuelle : vu qu'on lit/écrit par paquets de données, cela signifie qu'on traverse une page mémoire plus vite, ce qui fait que les défauts de page sont lus fréquents. Un processeur SIMD a intérêt à être combiné à une RAM et des caches solides, très performants. C'est le cas sur les processeurs modernes, mais cela a pose des problèmes pour les processeurs vectoriels, et a mené à leur abandon progressif. Mais passons outre ce problème et regardons la seconde raison qui font que ce résultat est naïf : la loi d'Amdhal ! Toutes les portions d'un programme ne sont pas accélérées par des instructions SIMD, il y a des portions dont les calculs ont des dépendances de données, d'autres qui ne sont pas parallélisables, etc. Une partie du programme est donc impossible à véctoriser (i.e optimiser pour le SIMD), une l'autre l'est. Pour simplifier les explications, on suppose qu'une instruction SIMD prend le même nombre de cycles que son équivalent non-SIMD. Par exemple, une addition SIMD prend le même temps qu'une addition normale. Dans ce cas, la portion non-vectorisable du code reste inchangée, alors que la portion vectorisable est divisée par N, par la largeur du vecteur. On retombe sur une formulation identique à la loi d'Amdhal sur le fond. Une manière de quantifier le tout est d'utiliser la métrique de l'efficience SIMD. Elle est calculée en divisant deux grandeurs. La première est elle-même un rapport : c'est le gain obtenu en terme de nombre d'instructions exécutées. Prenons un programme qui exécute I instructions avant vectorisation, et qui en exécute I/X après. Le gain s'exprime comme suit : : <math>G = \frac{I}{I/X} = X</math> Maintenant, prenons ce gain est divisons-le par la largeur d'un vecteur. On obtient alors l'efficience SIMD : : <math>\text{Efficience SIMD} = \frac{G}{N} = \frac{\frac{I}{I/X}}{N}</math> Notons que le gain et la largeur SIMD ne sont égales que dans un cas bien précis : tout le code est vectorisable. Il s'agit donc d'une reformulation du gain de la loi d'Amdhal, mais dans laquelle le pourcentage de code série est caché. Il est intéressant de regarder l'efficience SIMD quand on augmente la portion du code série, ainsi que la taille des vecteurs. Prenons un cas assez généreux, réaliste pour certaines applications très adaptées au SIMD : 1% du code est non-vectorisable, 99% profite du SIMD. L'efficience SIMD varie alors assez rapidement avec la taille des vecteurs. De 99% pour N=2, elle descend à 98% pour N=16, et tombe sous les 50% pou N=128. La conséquence est que les très grandes tailles de vecteurs ne sont pas vraiment utiles, le rendement est décroissant avec la taille des vecteurs. ==Les processeurs SIMD à registres généraux== Le premier type de processeur SIMD que nous allons voir est celui des processeurs de type '''SWAR''' (''SIMD Within A Register''). Le terme SWAR est un terme polysémique dont le sens a beaucoup changé au fil du temps. Ici, nous allons l'utiliser dans la définition la plus stricte : celle où on effectue du SIMD dans les registres généraux du processeur, sans registres spécialisés. Il s'agit d'une forme de SIMD qui est aujourd'hui peu utilisée, mais qui a été la première forme de SIMD utilisée dans des processeurs grand public commerciaux. L'idée est d'améliorer un petit peu un processeur normal, non-SIMD. Un processeur usuel contient des registres généraux de 32 à 64 bits, qui stockent chacun un opérande de même taille que le registre. D'anciens processeurs avaient des instructions pour effectuer des calculs simples, sur des opérandes plus courts. Par exemple, les processeurs x86 32 bits sont capables de faire des calculs sur 8, 16, ou 32 bits. Mais on ne pouvait placer qu'un seul opérande de 8, 16 ou 32 bits dans un registre général, ce qui est un léger gâchis. Le SWAR est une amélioration de cette technique qui vise à mieux utiliser les registres et l'ALU. L'idée est de regrouper plusieurs opérandes de 8, 16, voire 32 bits, dans un seul registre général. De plus, on ajoute des instructions SIMD d'addition/soustraction/autres, qui lisent des opérandes 8/16/32 bits dans ces registres généraux. Par exemple, sur un processeur 32 bits, on peut ajouter une opération d'addition qui lit deux registres de 32 bits, pour récupérer quatre opérandes de 16 bits (deux par registre) et effectue deux additions 16 bits en même temps dans l'ALU. Et il y a la même chose pour d'autres opérations, comme la soustraction, la comparaison, etc. Les instructions SIMD disponibles sur ces processeurs se limitent généralement à des additions, soustractions, comparaisons, mais guère plus. De plus, il s'agit d'instructions entières, il n'y a pas d'instructions SIMD flottantes. La raison à cela est très simple, mais nous l'expliquerons plus bas, dans la section sur l'implémentation. Disons simplement que de tells architectures visent une économie en circuit maximale. Elles implémentent les instructions SIMD en utilisant le moins de circuits possibles, ce qui fait qu'elles réutilisent les registres généraux et n'ont pas de registres SIMD séparés. ===Les extensions/jeux d'instructions de type SWAR=== Les premiers processeurs à intégrer du SWAR étaient les processeurs DEC Alpha, avec l'extension multimédia '''''Motion Video Extensions'''''. Les processeurs en question étaient les processeurs Alpha 21164PC (PCA56 and PCA57), Alpha 21264 (EV6) and Alpha 21364 (EV7). Les instructions SIMD disponibles étaient très simples et se résumaient à quelques comparaisons : trouver le maximum ou le minimum de deux opérandes de 8/16 bits, quelques instructions de permutation. Les extensions '''''Multimedia Acceleration eXtensions''''' des processeurs Hewlett-Packard PA-RISC sont aussi de ce type, mais disposent de plus d'instructions SIMD. La première version, appelée MAX-1, était disponible sur les processeurs 32 bits de la marque, à partir du processeur PA-7100LC. Les instructions disponibles étaient des additions et soustractions d'opérandes 16 bit. Il y avait en tout trois instructions d'addition et trois pour la soustraction. En tout, il y avait : * une instruction d'addition/soustraction non-signée utilisant l'arithmétique modulaire ; * une instruction d'addition/soustraction signée en arithmétique modulaire ; * une instruction d'addition/soustraction signée en arithmétique saturée. Outre les additions/soustractions, il y avait aussi une instruction pour calculer la moyenne de deux opérandes 16 bits, ainsi qu'une addition fusionnée avec un décalage. La seconde version, appelée MAX-2, ajouta des instructions d'additions fusionnées avec des décalages, mais aussi des instructions de permutation. De plus, cette version était disponible sur les processeurs 64 bits de la marque, ce qui fait que les registres étaient eux aussi de 64 bits. Ils pouvaient contenir deux fois plus d'opérandes 16 bits. Il n'y avait de gestion d'opérandes 32 bits pour les extensions SIMD. ===L'implémentation matérielle=== L'avantage de cette technique est la grande simplicité d'implémentation. On n'ajoute pas de registres SIMD séparés, ce qui a de nombreux avantages : économie de circuits car pas besoin d'un second banc de registre, pas besoin de sauvegarder des registres SIMD en plus lors d'un appel système/interruption, etc. L'implémentation a juste besoin de modifier le décodeur et l'unité de calcul. Le décodeur est modifié pour ajouter des instructions en plus, rien de spécifique au SWAR. Les modifications de l'unité de calcul sont spécifiques au SWAR et modifient la manière dont elle gère les retenues. Une première solution est d'utiliser une ALU bit-slicée, ce qui est l'idéal pour les unités de calcul entières. Par exemple, pour une ALU 32 bits entière, on peut la découper en 4 unités de calcul 8 bits. Pour une opération SIMD avec 4 opérandes 8 bits, les quatre ALU fonctionnent en parallèle, il n'y a pas transmission des retenues. Pour les calculs SIMD avec des opérandes de 16 bits, on regroupe les ALU par paire et on propage les retenues à l'intérieur d'une paire, mais pas entre les paires. Enfin, pour un calcul non-SIMD, les opérandes de 32 bits, on propage les retenues normalement, d'une ALU 8 bits vers la suivante. Une autre solution prend un additionneur 32/64 bits normal, mais mets à 0 les retenues. L'implémentation est très simple avec un additionneur à anticipation de retenues, où les retenues sont calculées en avance, avant de faire l'addition proprement dite, par un circuit d'anticipation de retenue. Il suffit alors d'ajouter un circuit de masquage en sortie du circuit d'anticipation de retenue, qui met à zéro les retenues adéquates. La gestion des débordements demande d'ajouter des circuits, ou du moins de modifier ceux déjà présents dans l'ALU. Typiquement, les circuits pour gérer l'arithmétique saturée sont un peu modifiés pour effectuer la mise à 1111...111 octet par octet et chaque octet est masqué si besoin. [[File:ALU modifiée pour implémenter du SWAR.png|centre|vignette|upright=3|ALU modifiée pour implémenter du SWAR]] Notons que la technique ne s'applique qu'aux additions, et aux opérations dérivées comme la soustraction et les comparaisons (ces dernières sont des soustractions déguisées). Mais les multiplications ou divisions ne peuvent pas s'implémenter simplement en modifiant des ALU existantes, du moins pas avec des modifications simples. Aussi, les processeurs qui utilisent cette technique se bornent aux additions et opérations dérivées, pas plus. La gestion des opérations de permutation est quant à elle très simple : beaucoup de processeurs disposent déjà d'instructions de permutation d'octets, qui sont l'équivalent d'instructions SIMD de permutation pour les registres généraux. Nous en avions déjà parlé dans le chapitre sur le langage machine et l'assembleur, quand nous avions fait la liste des instructions les plus courantes. De telles instructions de permutation d'octet sont utiles pour gérer le boutisme ou effectuer quelques manipulations assez rares. Pas besoin de rajouter une ALU dédiée, celle-ci est déjà présente de base, du moins si les instructions de permutation d'octet sont déjà présentes. ==Les processeurs SIMD purs (''packed SIMD'')== Maintenant que nous avons vu les instructions SIMD, passons maintenant aux processeurs eux-même. Tous les processeurs que nous allons voir dans ce qui suit supportent des instructions SIMD. Mais leur implémentation matérielle, leur micro-architecture, n'est pas la même. Ils ont aussi quelques différences en termes de jeu d'instruction, mais qui sont fortement liées à l'implémentation matérielle. Nous allons ici voir les '''processeurs SIMD purs''', aussi dits de type ''Packed SIMD''. Ils implémentent des instructions SIMD sans rien de plus, juste le strict minimum : pas de prédication, pas de vecteurs de taille variable, presque pas d’instructions horizontales, architecture de type LOAD-STORE. De tels processeurs utilisent plusieurs ALU entières/flottantes qui travaillent en parallèle. De plus, ils disposent de registres SIMD séparés des registres généraux. Voyons ces points immédiatement. * La première distinction est que les vecteurs sont stockés dans des registres séparés des registres généraux, appelés les '''registres SIMD''', ou ''registres vectoriels''. Ils sont beaucoup plus larges que les registres classiques et mémorisent 2, 4, 8 entiers/flottants. * La seconde distinction est que les calculs SIMD sont effectués dans une unité SIMD séparée de l'ALU ou de la FPU. L'unité de calcul SIMD peut exécuter au minimum des instructions SIMD verticales, plus rarement des instructions SIMD horizontales. Pour ce faire, elle regroupe plusieurs additionneurs/multiplieurs, qui effectuent des calculs en parallèle. ===Les instructions SIMD verticales : plusieurs ALU travaillant en parallèle=== [[File:SIMD2.svg|vignette|Parallélisme de données au niveau de l'unité de calcul. Celle-ci contient plusieurs circuits indépendants qui appliquent la même opération sur des données différentes.]] Une unité de calcul SIMD gère au minimum des instructions SIMD verticales. Et pour ce faire, elle contient plusieurs additionneurs/multiplieurs séparés. Par exemple, pour additionner deux vecteurs contenant chacun 16 entiers/flottants, il faut utilise 16 additionneurs entiers et 16 additionneurs flottants. Dans le cas général, une ALU SIMD est composée de plusieurs ALU entières et flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. Notons que les ALU travaillent en parallèle, elles font des calculs indépendants. Notons que les additionneurs dans chaque ALU doivent pouvoir être configurés de manière à gérer des tailles de données différentes. Par exemple, si on prend un vecteur simple, qui peut contenir soit 32 entiers de 16 bits, soit 16 entiers 32 bits, soit 8 entiers 64 bits, alors on doit utiliser 8 additionneurs, mais chacun d'entre eux doit pouvoir être reconfiguré de manière à ne pas propager les retenues au-delà des premiers 16 ou 32 bits. Un défaut de cette organisation est que le cout en circuits est loin d'être négligeable. Il faut dupliquer des unités de calcul et les coller en rajoutant des circuits, cela utilise beaucoup de transistors. Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures. Retenez cependant : l'usage de plusieurs ALU travaillant en parallèle a plusieurs défauts. Premièrement, cela implique des vecteurs de taille fixe, du fait que le nombre d'ALU travaillant en parallèle est fixe. Deuxièmement, des difficultés d'implémentation concernant les instructions de réduction. Détaillons ce second point ! Une autre possibilité est d'utiliser moins d'unités de calcul qu'il n'y a d'éléments dans un vecteur. Par exemple, pour des vecteurs contenant 32 flottants, il se peut qu'il n'y ait que 16 ALU. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale. L'avantage est que l'ensemble des calculs se fait en une seule instruction machine. L'implémentation est généralement la suivante : le processeur décode une instruction SIMD en deux micro-instructions SIMD plus courtes. Par exemple, pour une instruction SIMD de 32 éléments, elle sera décodée en deux instructions SIMD de 16 éléments, chacune étant exécutée sur l'ALU. Il est possible de réduire fortement l'impact en performance en doublant la fréquence de l'ALU. Par exemple, pour des vecteurs contenant 32 flottants, le processeur incorpore 16 ALU, mais celles-ci fonctionne à une fréquence double de celle du processeur. L'usage d'une ALU à double fréquence est une technique qui a été utilisée sur le Pentium 4 et quelques processeurs, pour des instructions non-SIMD. Mais pour les processeurs SIMD, elle s'applique à la perfection. L'implémentation est la même que précédemment, sauf que le ''scheduler'' fonctionne à double fréquence. Pour limiter la casse, il est préférable d'utiliser un ''scheduler'' séparé pour les instructions SIMD, qui sera seule à fonctionner à double fréquence. ===Les instructions horizontales : une ALU simple séparée=== Les instructions horizontales posent des difficultés d’implémentation. Elles ne peuvent pas s'implémenter en utilisant plusieurs ALU travaillant en parallèle, contrairement aux instructions verticales, ce qui fait qu'elles utilisent généralement des unités de calcul spécialisées. Et c'est là que la différence entre instructions de manipulation de vecteurs et instructions de réduction vient encore une fois poser problème. Les instructions de manipulation de vecteur sont relativement simples à implémenter : elles ont juste besoin d'une ALU spécialisée, relativement simple, composée de beaucoup de multiplexeurs à configurer convenablement. Mais pour les instructions de réduction, c'est autre chose ! L'implémentation des instructions de réduction est possible mais a un cout en circuits assez prohibitif. Pour additionner N entiers, il faut utiliser un additionneur multi-opérande qui prend beaucoup de circuits et dont le temps de calcul est proche de celui d'une multiplication (très lent). Trouver le maximum et le minimum d'un nombre demandent de faire la même chose avec des comparateurs. Le tout a un cout en circuit non-négligeable, pour accélérer des opérations assez peu fréquentes. Elles sont surtout utilisées pour trouver la somme ou le maximum/minimum d'un tableau, opération séquentielle par nature, avec peu parallélisme de données, peu courante. Une autre solution utilise une unité SIMD normale, mais intègre un système de contournement, de ''bypass'', qui relierait l'entrée d'une ALU à la sortie d'une autre. Mais le cout lié aux interconnexions est très important : on doit relier chaque ALU à toutes les autres dans le pire des cas, on peut optimiser le tout et réduire un peu la quantité d'interconnexions, mais le cout reste important. Au final, on gagne en circuits ce qu'on perd en interconnexions. Le choix entre les deux solutions est loin d'être facile et dépend du processeur, de la technologie utilisée, du budget en transistors disponibles, etc. Les processeurs SIMD purs résolvent ce problème assez simplement : ils n'implémentent pas d'instructions de réduction. Par contre, ils implémentent systématiquement des instructions de manipulation de vecteur, comme des permutations ou autres. La raison est que l'on peut émuler les instructions de réduction en utilisant des instructions SIMD de manipulation de vecteur couplées à des additions/multiplications SIMD verticales. Il s'agit donc d'un compromis entre cout en circuits et performance finale. Niveau circuits, on a une unité SIMD avec plusieurs ALU de calcul en parallèle, une unité de manipulation de vecteur séparée assez simple et au cout en circuits raisonnable. Les performances pour les opérations de réduction sont acceptables, le cout en performance est relativement modéré. ===Le banc de registre vectoriel=== La présence de registres vectoriels séparés permet d'avoir des vecteurs assez longs. Pour ce faire, les registres vectoriels sont plus grands que les registres généraux. Par exemple, sur un processeur 32 bits, qui gère donc des entiers de 32 bits, les vecteurs peuvent faire 128 ou 256 bits. Un vecteur contient donc plusieurs entiers ou flottants de taille maximale. On n'est pas dans le cas précédent, où registres généraux et SIMD sont les mêmes, ils sont séparés et n'ont pas la même taille. Le processeur utilise un banc de registre séparé pour les registres SIMD, appelé le '''banc de registre SIMD'''. Vu que les registres vectoriels sont assez longs, le banc de registre vectoriels a une grande taille, une grande capacité mémoire. Pour donner un exemple, les extensions SSE des processeurs x86 définissent 8 registres SIMD de 128 bits chacun. Ils ont été introduits à une époque où les processeurs avaient des registres généraux de 32 bits. Le banc de registres associé était donc environ 4 fois plus grand que celui pour les registres généraux. Je dis environ car le décodeur avait sensiblement la même taille, seul le plan mémoire était grossit, mais l'idée est là. Une conséquence est que le banc de registres SIMD consomme beaucoup d'énergie et d'électricité. Il en est de même avec les ALU SIMD. Et diverses optimisations tentent de réduire ce cout en énergie. L'une d'entre ajoute une mémoire cache entre les ALU SIMD et les registres vectoriels ! L'idée est que si un registre vectoriel est relu plusieurs fois de suite, la première lecture lira le vecteur dans le banc de registres SIMD, mais les lectures suivantes liront le vecteur dans le cache. L'optimisation marche car accéder au banc de registre vectoriel est vraiment couteux, plus qu'accéder à un cache. Le cache en question est appelé un '''''register reuse caches'''''. Les ''register reuse caches'' sont présents sur le processeur Monaka de Fujitsu, mais aussi et surtout dans les cartes graphiques modernes. Nous ferrons l'impasse sur les GPU modernes, car leur usage est quelque peu particulier. Il ne servent pas que à réduire la consommation d'énergie, mais aussi à réduire les conflits d'accès au banc de registre, qui est en réalité composé de plusieurs banques indépendantes. Leur usage sur le CPU Monaka est plus simple : ils ne servent vraiment qu'à réduire la consommation d'énergie, rien de plus. : Pour ceux qui veulent en savoir plus pour leur utilisation sur les cartes graphiques, tout est détaillé dans mon wikilivre sur les cartes graphiques, via le lien suivant. Mais sachez cependant qu'il faut idéalement lire le chapitre entier, et peut-être même les chapitres précédents, pour comprendre cette section. Voici le lien : [https://fr.wikibooks.org/wiki/Les_cartes_graphiques/La_microarchitecture_des_processeurs_de_shaders#L'Operand_Collector_et_les_caches_de_register_reuse L'Operand_Collector_et_les_caches_de_register_reuse] ===L'implémentation des LOAD-STORE à des données consécutives=== Les architectures SIMD pures sont des architectures de type LOAD-STORE, ce qui veut dire que les instructions SIMD ne peuvent que lire ou écrire dans les registres vectoriels, pas en mémoire ou ailleurs. Les transferts entre registres SIMD et mémoire sont le fait d'instructions LOAD et STORE spécialisées, qui transfèrent des vecteurs entre RAM et registres vectoriels. Les autres architectures n'ont pas nécessairement ce genre de restrictions, mais laissons cela pour plus tard. Implémenter les instructions LOAD/STORE pour des vecteurs dont les données sont consécutives en mémoire n'est pas très compliqué. Il suffit juste d'élargir le port de lecture/écriture du cache L1 de données, pour pouvoir lire/écrire un vecteur entier. Rien de bien compliqué, il suffit juste d'ajouter des interconnexions et d'ajouter des multiplexeurs. Du moins, c'est le cas tant que les vecteurs sont plus petits qu'une ligne de cache, ce qui est souvent le cas en pratique. Dans le cas très rare où les vecteurs sont plus longs qu'une ligne de cache, les LOAD/STORE doivent se faire en deux accès dans le cache, ce qui demande d'ajouter du matériel pour contrôler la situation. Précisons cependant qu'il s'agit là d'un cas idéal, où les vecteurs sont correctement alignés en mémoire. En effet, il se peut qu'un vecteur soit à cheval sur deux lignes de cache, alors qu'il est plus petit qu'une ligne de cache. Il suffit pour cela qu'il soit placé à une adresse adéquate. Par exemple, prenons un vecteur de 16 octets et des lignes de cache de 32 octets. Si le vecteur démarre à l'adresse 24, alors il sera à cheval sur deux lignes de cache. Il faut donc intégrer des circuits pour gérer cette situation. Pour cela, il suffit d'ajouter un registre pour stocker la première de ligne lue, puis un circuit qui combine les deux lignes de cache pour donner le vecteur final. ===L'implémentation des LOAD-STORE en ''scatter-gather''=== L'implémentation des accès en ''scatter-gather'' est plus compliquée. Un accès en ''scatter-gather'' demande de faire des calculs d'adresse, suivis par un ensemble de lectures/écritures, suivis par un regroupement des données lues dans un vecteur. Le calcul d'adresse peut se faire en parallèle dans une unité de calcul SIMD, dans plusieurs unités de calcul d'adresse en parallèle. Par contre, les lectures/écritures ne le peuvent pas. Le cache n'a pas assez de ports de lecture/écriture pour lire/écrire N données. Il a quelques ports, ce qui lui permet de lire/écrire 3/4 données grand maximum, soit moins que ce qui est nécessaire pour faire un accès en ''scatter-gather'' en une fois. Les lectures/écritures sont donc effectuées en plusieurs fois. Pour une opération de ''Gather'', il faut aussi regrouper les données lues dans un vecteur. Sur les caches des processeurs à haute performance, le calcul d'adresse est intégré dans le décodeur. Ce sont des caches adressés par somme, que nous avions abordé dans le chapitre sur les mémoires caches. Avec de telles caches, on ne peut pas utiliser une unité de calcul d'adresse SIMD, on ne peut calculer qu'autant d'adresse qu'il y a de ports sur le cache. Les accès en ''scatter-gather'' regroupent plusieurs accès mémoire, qui peuvent être effectués dans le désordre. La seule exception est celle où un ''scatter'' effectue plusieurs écritures à la même adresse, où les deux écritures doivent se faire dans l'ordre allant du premier au dernier élément du vecteur d'adresse. Les processeurs SIMD profitent de cette absence d'ordre pour effectuer les accès dans un ordre le plus optimisé possible. Par exemple, imaginons le cas où une instruction ''gather'' lise des éléments placés dans deux lignes de cache uniquement, mais dans le désordre en passant sans cesse d'une ligne de cache à l'autre. Il est alors possible de faire tous les accès dans la première ligne de cache en premier, avant de faire ceux allant dans l'autre. Une telle optimisation marche aussi bien pour les lectures que les écritures, pour les ''gather'' que pour les ''scatter''. Elle peut aussi être adaptée pour plus de deux lignes de cache. Elle porte le nom de '''''memory coalescing'''''. Une autre optimisation possible est de détecter le cas où un accès en ''scatter-gather'' accède à des données consécutives. Il suffit pour cela que les indices/adresses du vecteur opérande soient consécutives, et cela arrive plus souvent qu'on ne le pense. Dans ce cas, l'accès est remplacé par une instruction LOAD/STORE SIMD normale, beaucoup plus simple et plus rapide. Les deux optimisations demandent que les adresses soient calculées avant de faire l'accès mémoire. Les adresses calculées sont alors comparées entre elles, par un réseau de comparateurs assez complexe. Un point important de l'implémentation des instructions ''gather'' est qu'une donnée chargée doit être insérée au bon endroit dans le vecteur destination. Et pareil pour les ''scatter'' : il faut lire la bonne donnée au bon moment. Si on prend une ligne de cache complète, il faut lire plusieurs élèments en même temps et les envoyer au registre de destination au bon endroit. Il faut pour cela tout un réseau de multiplexeurs assez complexe pour faire ce travail. Il y a le même genre de réseau pour les instructions ''scatter'', mais qui va dans l'autre sens : du registre vers la ligne de cache. ==Les processeurs SIMD avec prédication== Un obstacle très gênant à la vectorisation est la présence de branchements conditionnels dans les boucles à vectoriser. Si une boucle contient des branchements conditionnels, elle ne peut pas être vectorisée facilement : il est impossible de zapper certains éléments d'un vecteur suivant une condition. Il n'est pas possible d'effectuer une instruction SIMD seulement sur certains éléments d'un vecteur et d'ignorer les autres, en fonction du résultat d'un branchement conditionnel. Tout le vecteur y passe, ou le vecteur est épargné, mais la condition intermédiaire n'est pas possible avec du SIMD simple. Pour résoudre ce problème, les processeurs SIMD incorporent diverses techniques pour implémenter ou éviter les branchements conditionnels le plus possible. La plus simple est l'incorporation de certaines instructions SIMD simples, comme la valeur absolue, le calcul du minimum/maximum, etc. Ces opérations sont généralement émulées en utilisant des branchements, avec quelques conditions simples. Incorporer ces instructions permet ainsi de faire disparaitre une partie des branchements du code. Elle ne paye pas de mine, mais elle a un résultat pas négligeable pour le rendu graphique, certaines applications de calcul scientifiques, et quelques autres. Son cout en transistors est très variable, il faut rajouter des circuits dans le séquenceur, l'unité de calcul SIMD est assez peu modifiée (il faut juste rajouter des multiplexeurs). Le vrai problème est que cette solution laisse énormément de branchements dans le code. Et qu'il faut donc trouver une solution plus générale, capable de réduire drastiquement les branchements. Pour cela, on réutilise une technique vue dans les chapitre sur l'assembleur et le langage machine : la prédication. ===Les instructions à prédicats=== Pour optimiser le cas général, il est possible d'utiliser des instructions à prédicats adaptées pour fonctionner sur des vecteurs. L'idée est que quand une instruction effectue un calcul sur un ou deux vecteurs, certains éléments du vecteurs sont ignorés. Les éléments à ignorer sont choisis suivant le résultat d'une instruction de comparaison, qui effectue un test : les éléments pour lesquels ce test est respecté sont pris en compte, ceux qui ne passent pas le test sont ignorés. Pour donner un exemple d'utilisation, imaginons que l'on ait un vecteur dans lequel on veut remplacer toutes les valeurs négatives par des 0. Dans ce cas, on utilise : * une instruction de comparaison, qui compare chaque élément du vecteur avec 0 et génère plusieurs bits de résultat ; * suivi d'une instruction à prédicat qui met à zéro les éléments pour lesquels les bits de résultat précédents sont à 1. La prédication peut aussi être utilisée pour simuler des vecteurs de taille variable. Tous les vecteurs ont une taille fixe, mais on peut ne pas faire de calculs sur la fin d'un vecteur, ce qui fait que tout se passe comme si le vecteur était plus petit. Ce n'est pas sa seule utilisation, mais la prédication permet de simuler ce comportement. Néanmoins, cela n'en fait pas un processeur vectoriel, capable de traiter des vecteurs de taille variable : il n'y a pas de moyen de préciser explicitement la taille des vecteurs à traiter. Tout cela demande plusieurs modifications du jeu d'instruction : ajouter des instructions de comparaison SIMD, ajouter de quoi faire la prédication. ===Le calcul des masques et le registre de masque=== La prédication demande que le résultat d'une comparaison soit stocké dans un registre à prédicat ou dans le registre d'état. Pour les processeurs SIMD, il y a cependant une grosse adaptation à faire concernant le fonctionnement des comparaisons et le stockage des résultats. Premièrement, les instructions de comparaison SIMD comparent une paire de deux éléments à la même place dans deux vecteurs et fournissent un résultat d'un bit pour chaque paire. Le résultat est donc un ensemble de bits, qui sont regroupées dans un vecteur spécialisé. Deuxièmement, reste à savoir que faire de ce vecteur. Pour cela, deux solutions sont possibles. Avec la première, le vecteur est stocké dans un registre vectoriel/SIMD comme les autres, il est traité comme n'importe quel autre vecteur. Avec la seconde solution, il reçoit un registre dédié, qui est un mix entre registre d'état et registre à prédicat, appelé un '''registre de masque''' (''Vector Mask Register''). Il stocke un bit pour chaque donnée présente dans le vecteur à traiter, qui indique s'il faut ignorer la donnée ou non. L'instruction SIMD suivante fait usage de ce masque pour savoir s'il faut faire l'opération pour chaque élément du vecteur. [[File:Vector mask register.png|centre|vignette|upright=2|Vector mask register]] Il peut y avoir un ou plusieurs registres de masque. S'il y en a plusieurs, ils sont généralement nommés, sur le modèle des registres à prédicats. L'application d'un masque demande de fournir le nom du registre de masque à utiliser, ils adressés explicitement dans les instructions. S'il n'y en a qu'un seul, il est possible de le pas le nommer et de faire en sorte qu'il soit adressé implicitement par les instructions qui en ont besoin. On parle alors de '''masquage implicite'''. Le masquage implicite est très utilisé sur les cartes graphiques, qui sont actuellement des architectures SIMD, comme nous le verrons plus bas. Un gros problème du masquage implicite est qu'il introduit des dépendances implicites entre instructions, qui ne sont pas explicites quand on regarder uniquement les registres manipulés par une instruction, qui perturbent le renommage de registres. Rien d'insurmontable, mais cela peut poser des problèmes de performance et la résolution de ce problème a un cout en hardware. Mais ce n'est un problème que sur les architectures à exécution dans le désordre. Les architectures à émission dans l'ordre ne sont pas concernées, et c'est notamment le cas sur les GPU et les cartes graphiques modernes. Diverses optimisations sont possible avec un registre de masque. La première est de ne pas exécuter d'instruction si tous les bits du masque sont à 0. Dans ce cas, l'instruction SIMD n'a pas de calculs à faire, vu que tous les résultats sont masqués. Il est alors possible de ne pas exécuter l'instruction SIMD et de passer directement à la suivante. L'implémentation matérielle est très simple : une porte NOR qui détermine si le registre de masque contient la valeur 0. La sortie de cette porte NOR est envoyée au séquenceur d'instruction. Le décodeur est alors conçu pour zapper l'instruction si le registre de masque contient un zéro. ===Les accès mémoire en ''scatter-gather'' avec prédication=== Un problème avec les accès mémoire que certains accès peuvent déclencher des défauts de page ou d'autres formes d'exceptions (erreurs de protection mémoire, autre). Si l'accès mémoire se fait sans prédication, ces exceptions sont gérées dans l'ordre des accès mémoire, en commençant par le début du vecteur, ce qui rend l'implémentation assez facile. Mais avec la prédiction, il se peut qu'un accès masqué et censé être ignoré déclenche une exception matérielle. Et le résultat dépend de l'implémentation. Une première solution est d'effectuer l'accès mémoire, et de masquer ensuite les éléments à ignorer. Mais dans ce cas, les éléments masqués peuvent déclencher des exceptions, qui ne sont pas censées se produire. Une autre solution est de déterminer quelles sont les adresses qu'il faut lire ou écrire et ne pas faire les accès mémoire masqués. Une autre solution effectue tous les accès avant de savoir quels sont ceux à masquer, mais de ne pas déclencher d'exception avant qu'on sache quels sont les accès masqués. Une fois le masquage des accès effectués, le processeur exécute les exceptions adéquates. Il s'agit d'un comportement de masquage d'exception, très utilisé. L'implémentation est assez simple pour les accès mémoire contiguës, mais plus compliqué pour les accès en ''stride'' ou en ''scatter-gather''. Pour les accès contiguës, les seules exceptions pouvant survenir sont celles ayant lieu quand on passe d'une page mémoire à la suivante (au sens pagination, mémoire virtuelle). Et le hardware pour détecter un débordement de page est assez simple. Mais pour les accès en ''stride'' ou en ''scatter-gather'' demandent qu'oin vérifie les exceptions pour chaque élément du vecteur. ===L'implémentation matérielle=== L'implémentation matérielle est assez proche des processeurs SIMD purs. On retrouve plusieurs unités de calcul travaillant en parallèle, avec quelques circuits ajoutés pour gérer la prédication. Le cout supplémentaire en circuits est acceptable, les gains en performance associés sont très intéressants. Les circuits en plus, pour gérer la prédication, sont similaires à ceux utilisés pour la prédication sur les architectures non-SIMD, mais sont dupliqués. La seule innovation architecturale est l'implémentation du registre de masque. Tous les processeurs n'en utilisent pas, mais c'est la solution la plus performante. En effet, ce registre de masque est un peu l'équivalent du registre d'état pour les instructions SIMD, bien qu'il soit techniquement une concaténation de registres à prédicat non-nommés. Ils ont pour avantage de libérer des registres SIMD normaux tout en ayant un cout en matériel très limité. Un autre point est que pour gérer les masques, les instructions SIMD à prédicat doivent lire le masque. Sans registre de masque, en utilisant un registre SIMD normal, il faut rajouter un troisième port de lecture sur le banc de registre pour récupérer le masque, ce qui est couteux en matériel. Pas besoin avec un registre de masque, qui n'est connecté qu'à l'ALU, sur le même modèle que le registre d'état. Plus haut, on a vu que l'implémentation de l'ALU SIMD peut se faire en utilisant des moins d'ALU que prévu. Par exemple, pour des vecteurs de 32 éléments, on peut utiliser 16 ALUs (allant à double fréquence ou non). Une instruction SIMD est alors décodée en plusieurs micro-instructions SIMD consécutives, chacune traitant une partie d'un vecteur. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. Pas mal de processeurs x86 font cela pour les instructions AVX 512, qui travaillent sur des vecteurs de 512 bits. Les premières version de l'AVX travaillaient sur des vecteurs de 256 bits, l'AVX-512 étendit le tout à 256 bits. Lors de l'introduction de l'AVX, la plupart des CPU x86 avaient une ALU 256 bits et les concepteurs de processeurs ont préféré la garder telle quelle. Les ALU SIMD travaillent donc sur des vecteurs de 256 bits, les instructions 25- bits étaient traitées comme une seule µops et les instructions 512 bits avec deux µops de 256 bits. Mieux que ca, une partie des registres sont restés en 256 bits, et cela a été masqué par le renommage de registres. L'AVX-512 supporte 32 registres AVX de 512 bits, les registres 256 bits de l'AVX normal étant inclus dedans. Avec le renommage de registre, on peut facilement multiplier par 10 le nombre de registres supportés, mais le nombre exact varie selon que l'on parle des registres 256 bits ou 512 bits. Par exemple, les microarchitectures Intel avaient en tout 327 registres 256 bits, mais seulement 242 pour les registres 512 bits. L'avantage est que cela se marie bien avec l'abandon des opérations pour les masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit vérifier que les 8 bits de poids faible ainsi que de poids fort du registre de masque ne soient pas individuellement à zéro. Cela se fait en dupliquant la porte NOR évoquer plus haut, l'une pour la partie basse du registre et la seconde pour la partie haute. ==La prédication pour les structures de contrôle imbriquées== Au niveau du jeu d’instruction, les architectures SIMT implémentent de la prédication, sous une forme améliorée. Les processeurs SIMT actuels sont surtout utilisées sur les processeurs intégrés aux cartes graphiques. Et ces derniers gèrent très mal les branchements, et encore : beaucoup de cartes graphiques, même récentes, ne gèrent tout simplement pas les branchements. Elles doivent donc se débrouiller avec uniquement la prédication, là où les processeurs SIMD utilisent des branchements normaux en complément de la prédication. Insistons sur le fait que cet usage exclusif de la prédication n'est présent que sur une sous-partie des architectures SIMT, le seul exemple que l'auteur de ce wikilivre connait étant celui des cartes graphiques. Les architectures SIMT sans branchements doivent donc trouver des solutions pour gérer les structures de contrôle imbriquées, à savoir une boucle placée à l'intérieur d'une autre boucle, un IF...ELSE dans un autre IF...ELSE, etc. Elles utilisent pour cela la prédication, combinée avec des mécanismes annexes. Le premier d'entre eux est l'usage de plusieurs registres de masques organisés d'une manière bien précise, l'autre est l'usage de compteurs d'activité. Voyons ces deux techniques. ===La pile de masques=== La '''pile de masques''' remplace le ou les registres de masque. Sans elle, le processeur SIMD incorpore un registre de masque qui est adressé implicitement ou explicitement. Éventuellement, le processeur peut contenir plusieurs registres de masque séparés adressables via un nom de registre. Avec elle, le processeur SIMD incorpore plusieurs registres de masque organisé en pile. Le registre de masque est donc remplacé par une mémoire LIFO, une pile, dans laquelle plusieurs masques sont empilés. Le tout forme une pile, similaire à la pile d'appel, sauf qu'elle est utilisée pour empiler des masques. Un masque est calculé et empilé à chaque entrée dans une structure de contrôle, puis dépilé une fois la structure de contrôle exécutée. L'empilement et le dépilement des masques est effectué par des instructions PUSH et POP, présentes dans le jeu d'instruction du processeur SIMD. Le calcul des masques doit répondre à plusieurs impératifs. * Premièrement, chaque masque se calcule en faisant un ET entre le masque précédent et le masque calculé par l'instruction de test. Cela permet de ne pas réveiller d’élément au beau milieu d'une structure imbriquée. Si in IF désactive certains éléments du vecteur, une condition imbriquée dans ce IF ne doit pas réveiller cet élément. Le fait de faire un ET entre les masques garantit cela. * Deuxièmement, les masques doivent être empilés et dépilés correctement. Au moment de rentrer dans une structure de contrôle, on effectue une instruction de test associée à la structure de contrôle, qui calcule un masque, et on empile le masque calculé. Au moment de sortir de la structure de contrôle, on dépile le masque en question. L'implémentation demande d'utiliser une mémoire LIFO pour stocker la pile de masques, et quelques circuits annexes. Il faut notamment un circuit relié à l'ALU qui récupère les conditions, les résultats des comparaisons, et qui effectue le ET pour combiner les maques. Pour donner un exemple, prenons le code suivant, qui est volontairement simpliste et ne sert qu'à des fins d'explication : <syntaxhighlight lang="c"> if ( condition 1 ) { if ( condition 2 ) { ... } else { ... } Autres instructions } Instructions après le IF... </syntaxhighlight> Imaginons que l'on traite des vecteurs de 8 éléments. Pour le vecteur considéré, la première condition (a > 0) n'est respectée que par les 4 premiers éléments. L'instruction de condition calcule alors le masque correspondant : 1111 0000. Le masque est alors calculé, puis empilé au somment de la pile. La seconde instruction de test, qui teste la variable b, est maintenant valide pour les 4 bits du milieu du masque. Mais n'allez pas croire que le masque correspondant soit 0011 11100 : il faut tenir compte de la condition précédente, qui a éliminé les 4 derniers éléments. Pour cela, on fait un ET logique entre le masque précédent, et le masque calculé par la condition. Le masque au sommet de la pile est donc lu, combiné avec le masque calculé par l'instruction, ce qui donne le masque final. Le masque final est alors empilé au sommet de la pile. On exécute alors l'instruction du IF, en tenant compte du masque qui est au sommet de la pile. Si le IF était plus compliqué, toutes les instructions suivantes tiendraient compte du masque. En fait, le masque est pris en compte tant qu'il n'est pas dépilé. Une fois que le IF est terminé, le masque est dépilé. On passe alors au ELSE, et rebelotte. Le masque pour le ELSE est calculé en combinant le masque au sommet de la pile avec la condition du ELSE. Le masque au sommet de la pile est celui calculé à l'entrée du premier IF, pas le second qui a été dépilé. Les instructions du ELSE sont alors exécutées en tenant compte de ce masque. Une fois qu'elles sont toutes exécutées, le masque est dépilé. Puis vient l'exécution des instructions après le ELSE. Elles utilisent le masque empilé au sommet de la pile, qui correspond à celui à l'entrée du IF. Puis vient le moment d'exécuter les instructions après le IF : pas de masque, on exécute sur tout le vecteur. ===Les compteurs d'activité=== Une variante de la technique précédente remplace la pile de masques par des '''compteurs d'activité'''. La technique est similaire, si ce n'est qu'elle utilise moins de circuits. Avant , on avait une pile de masques de même taille, dont les bits sont à 0 ou 1 suivant que la condition est remplie. La pile de masque ressemble donc à ceci : {|class="wikitable" |- ! masque 1 | 1 || 1 || 1 || 1 |- ! masque 2 | 0 || 1 || 1 || 1 |- ! masque 3 | 0 || 1 || 1 || 1 |- ! masque 4 | 0 || 0 || 0 || 1 |- ! masque 1 | colspan="4" | vide |} Une manière équivalent de représenter cette pile de masque est de compter combien de bits sont à 0 dans chaque colonne. Attention : j'ai bien dit à 0 ! On obtient alors : {|class="wikitable" |- ! masque 1 | 3 || 1 || 1 || 0 |} Et c'est le principe caché derrière la techniques des compteurs d'activité. Chaque élément dans un vecteur, chaque place, se voit attribuer un compteur. Un compteur non-nul indique qu'il ne faut pas prendre en compte l’élément. Ce n'est qu'une fois que le compteur est nul que l'on effectue des opérations sur l’élément associé du vecteur. À chaque fois qu'on entre dans une structure de contrôle, on teste une condition sur chaque élément. Si la condition est respectée pour un élément, alors le compteur ne change pas. Mais si la condition n'est pas respectée, alors on incrémente le compteur associé. En sortant de la structure de contrôle, on décrémente le compteur associé. Notons que les compteurs qui n'ont pas été incrémenté en entrant dans la structure de contrôle ne sont pas décrémenté en sortant. En clair, là où on empilait/dépilait un masque, on se contente d'incrémenter/décrémenter un compteur. Utiliser un compteur en lieu et place d'une colonne entière dans la pile de masque utilise moins de bits. Et c'est sans doute pour cette raison que certaines cartes graphiques, comme les cartes graphiques intégrées d'Intel depuis 2004, utilisent cette technique. ==Les processeurs vectoriels== Les '''processeurs vectoriels''' sont les ancêtres des processeurs à instruction SIMD. Bien qu'ils soient arrivés en premier, ils incorporent diverses techniques que de simples instructions SIMD n'ont pas forcément. C'est paradoxal, mais c'est ainsi. La première différence se manifeste au niveau du jeu d'instruction. Les processeurs vectoriels sont capables de traiter des vecteurs de taille variable, alors que les instructions SIMD usuelles ne gèrent que des vecteurs de taille fixe. Par taille variable, on veut dire que le processeur gère nativement des vecteurs d'une taille allant de 1 à la taille maximale d'un vecteur. La taille maximale d'un vecteur est celle permise par la taille des registres vectoriels, il s'agit de la taille d'un vecteur SIMD équivalent sur les autres processeurs SIMD. La seconde différence est une différence en termes de micro-architecture. Un processeur SIMD actuel dispose d'une unité de calcul très élaborée, capable de faire plusieurs calculs en parallèle, au moins un pour chaque élément du vecteur. Par exemple, si un vecteur contient 16 entiers, l'ALU SIMD doit contenir au moins 16 additionneurs entiers. Mais sur un processeur vectoriel, ce n'est pas le cas : l'unité de calcul est en réalité une unité de calcul entière/flottante normale, mais qui a la particularité d'être pipelinée. Les calculs sont donc effectués en parallèle, mais d'une manière totalement différente. La plupart de ces différences s'expliquent par le fait que les anciens processeurs vectoriels devaient faire avec des limitations en termes de circuits. Ils avaient peu de transistors à leur disposition, ce qui fait que leur unité de calcul devait être la plus simple possible. D'où l'utilisation d'une unité de calcul simple, mais utilisée de manière pipelinée. De plus, les processeurs vectoriels ne possèdent aucune mémoire cache pour les données et se contentent juste de caches d'instruction. De tels caches sont généralement peu utiles quand on manipule des tableaux, chose quasiment systématique quand on travaille avec du parallélisme de données. ===Une unité de calcul pipelinée=== La différence entre processeur vectoriel et SIMD tient dans la façon dont sont traités les vecteurs : les instructions SIMD traitent chaque élément en parallèle, alors que les processeurs vectoriels pipelinent ces calculs ! Par pipeliner, on veut dire que l’exécution de chaque instruction est découpée en plusieurs étapes indépendantes. Au lieu d'attendre la fin de l’exécution d'une opération avant de passer à la suivante, on peut commencer le traitement d'une nouvelle donnée sans avoir à attendre que l'ancienne soit terminée. [[File:Pipeline.png|centre|vignette|upright=2|Pipeline vectoriel.]] Pour donner un exemple, on peut donner l'exemple d'une multiplication flottante effectuée entre deux registres. Son exécution peut être décomposée en plusieurs étapes. Par exemple, on peut avoir 3 étapes : * une première étape E qui va additionner les exposants et gérer les diverses exceptions ; * une étape M qui va multiplier les mantisses ; * et enfin une étape A qui va arrondir le résultat. L’exécution de notre opération flottante sur un vecteur donnerait donc quelque chose dans le genre, où chaque ligne correspond au traitement d'un nouvel élément dans un vecteur, dans un paquet SIMD. [[File:Multiplication vectorielle - pipeline.png|centre|vignette|upright=2|Multiplication vectorielle - pipeline]] : Certains processeurs vectoriels utilisent plusieurs ALU pipelinées pour accélérer les calculs. On peut les voir comme des intermédiaires entre processeur vectoriels et SIMD SWAR. Avec une unité de calcul pipelinée découpée en N étages, on peut gérer N données simultanées : autant qu'il y a d'étapes différentes. Mais ce nombre maximal de données met un certain temps avant d'être atteint. L'unité de calcul met du temps avant d'arriver à son régime de croisière. Durant ce temps, elle n'a pas commencé à traiter suffisamment d’éléments pour que toutes les étapes soient occupées. Ce temps de démarrage est strictement égal du nombre d'étapes nécessaires pour effectuer une instruction. La même chose arrive vers la fin du vecteur, quand il ne reste plus suffisamment d’éléments à traiter pour remplir toutes les étapes. [[File:Startup and dead time vector pipeline.png|centre|vignette|upright=2|Startup and dead time vector pipeline]] Pour amortir ces temps de démarrage et de fin, certains processeurs démarrent une nouvelle instruction sans attendre la fin de la précédente : deux instructions peuvent se chevaucher pour remplir les vides. Par contre, il peut y avoir des problèmes lorsque deux instructions qui se suivent manipulent le même vecteur. Il faut que la première instruction ait fini de manipuler un élément avant que la suivante ne veuille commencer à le modifier. Pour cela, il faut avoir des vecteurs suffisamment qui contiennent plus d’éléments qu'il n'y a d'étapes pour effectuer notre instruction. ===La technique du ''chaining''=== La technique du pipeline peut encore être améliorée dans certains cas particuliers. L'idée est simplement d'ajouter la technique du contournement (''bypass'') à l'unité de calcul. Pour rappel, le contournement connecte la sortie de l'ALU à une de ses entrées, ce qui permet au résultat d'un calcul d'être réutilisable au cycle d’horloge suivant. L'usage du contournement sur les processeurs vectoriels porte le nom de '''''Vector Chaining'''''. Un processeur implémentant le ''chaining''/''bypass'' a toutes ses unités de calcul reliées entre elles : la sortie d'une unité est reliée aux entrées de toutes les autres. Pour comprendre à quoi peut servir cette technique, on peut citer deux grands exemples principaux. Le premier avantage est que l'implémentation des instructions SIMD horizontales est beaucoup plus simple. C'est pour cela que les processeurs vectoriels intègrent souvent des instructions horizontales, et notamment des instructions arithmétiques horizontales. Le contraste avec les processeurs SIMD récents, qui utilisent plusieurs ALU travaillant en parallèle, est frappant. Ces derniers n’intègrent pas d'instructions horizontales arithmétiques, les seules opérations horizontales supportées sont généralement des permutations ou des instructions ''compress/expand'', guère plus. Maintenant, introduisons une autre possibilité par un exemple. Dans cet exemple, on suppose que le processeur dispose d'une ALU pour l'addition et d'une autre pour la multiplication. Imaginons que l'on ait trois vecteurs nommés A, B et C. Pour chaque énième élément de ces paquets, je souhaite effectuer le calcul <math>A_n + B_n \times C_n</math>. En théorie, il faudrait faire d'abord la multiplication, stocker le résultat temporaire de la multiplication dans un registre vectoriel, puis faire l'addition. Mais en rusant un peu, on peut utiliser le pipeline plus efficacement. Une fois que le premier élément de la multiplication du premier vecteur est connu, pourquoi ne pas démarrer l'addition immédiatement après, et continuer la multiplication en parallèle ? Après tout, les deux calculs ont lieu dans des ALUs séparés. Et le contournement permet d'alimenter l'ALU d'addition avec la sortie du circuit multiplieur. [[File:Vector chaining.png|centre|vignette|upright=2|Vector chaining]] ===La gestion des vecteurs de taille variable=== L'usage d'une unité de calcul pipelinée fait que la taille des vecteurs n'est pas forcément fixe. Autant les processeurs SIMD non-vectoriels disposent d'un nombre fixe d'unités de calcul, et peuvent donc gérer des vecteurs de taille fixe, autant ce n'est pas le cas des processeurs vectoriels. Une unité de calcul pipelinée à 16 étage peut utiliser les trois premiers pour traiter un vecteur de 3 élements, les 5 suivants pour un second vecteur de 5, et le reste pour un troisième vecteur. Reste que la gestion des vecteurs de taille variable doit être gérée au niveau du séquenceur, mais surtout : doit être permise au niveau du jeu d'instruction. Pour cela, il faut ajouter au jeu d'instruction de quoi indiquer la taille du vecteur en cours de traitement. Et c'est le rôle d'un registre spécialisé, le '''Vector Length Register''', que de gérer les vecteurs de taille variable. Le ''Vector Length Register'' est un registre qui indique combien d’éléments on doit traiter dans un vecteur. On peut ainsi dire au processeur : je veux que tu ne traites que les 40 premiers éléments présents d'un paquet. Ils sont utilisés pour gérer des tableaux dont la taille n'est pas un multiple d'un vecteur. Quand on arrive à la fin d'un tableau, il suffit de configurer le ''Vector Length Register'' pour ne traiter que ce qu'il faut. [[File:Vector length register.png|centre|vignette|upright=2|Vector length register.]] Il facilite l'usage du '''déroulage de boucle''', une optimisation qui vise à réduire le nombre d'itérations d'une boucle en dupliquant son corps en plusieurs exemplaires. Le compilateur réplique le corps de la boucle (les instructions à répéter) en plusieurs exemplaires dans cette boucle, avant de corriger le nombre d'itérations de la boucle. Par exemple, prenons cette boucle, écrite dans le langage C : <syntaxhighlight lang="c"> int i; for (i = 0; i < 100; ++i) { a[i] = b[i] * 7 ; } </syntaxhighlight> Celle-ci peut être déroulée comme suit : <syntaxhighlight lang="c"> int i; for (i = 0; i < 100; i+=4) { a[i] = b[i] * 7 ; a[i+1] = b[i+1] * 7 ; a[i+2] = b[i+2] * 7 ; a[i+3] = b[i+3] * 7 ; } </syntaxhighlight> Les instructions vectorielles permettent de traiter plusieurs éléments à la fois, ce qui fait que plusieurs tours de boucles peuvent être rassemblés en une seule instruction SIMD. Le déroulage de boucles permet d'exposer des situations où ce regroupement est possible. Dans notre exemple, si jamais notre processeur dispose d'une instruction de multiplication capable de traiter 4 éléments du tableau a ou b en une seule fois, la boucle déroulée peut être vectorisée assez simplement en utilisant une multiplication vectorielle (que nous noterons vec_mul). <syntaxhighlight lang="c"> int i; for (i = 0; i < 100; i+=4) { vec_a[i] = vec_mul ( vec_b[i] , 7 ) ; } </syntaxhighlight> Le déroulage de boucles n'est toutefois pas une optimisation valable pour toutes les boucles. Reprenons l'exemple de la boucle vue plus haut. Si jamais le tableau à manipuler a un nombre d’éléments qui n'est pas multiple de 4, la boucle ne pourra être vectorisée, vu que la multiplication vectorielle ne peut traiter que 4 éléments à la fois. Pour ce faire, les compilateurs utilisent généralement deux boucles : une qui traite les éléments du tableau avec des instructions SIMD, et une autre qui traite les éléments restants avec des instructions non vectorielles. Cette transformation s'appelle le '''strip-mining'''. Par exemple, si je veux parcourir un tableau de taille fixe contenant 102 éléments, je devrais avoir une boucle comme celle-ci : <syntaxhighlight lang="c"> int i; for (i = 0; i < 100; i+=4) { a[i] = b[i] * 7 ; a[i+1] = b[i+1] * 7 ; a[i+2] = b[i+2] * 7 ; a[i+3] = b[i+3] * 7 ; } for (i = 100; i < 102; ++i) { a[i] = b[i] * 7 ; } </syntaxhighlight> Les processeurs vectoriels utilisent le ''Vector Length Register'' pour éviter d'utiliser le strip-mining. Avec ce registre, il est possible de demander aux instructions vectorielles de ne traiter que les n premiers éléments d'un vecteur : il suffit de placer la valeur n dans le ''Vector Length Register''. Évidemment, n doit être inférieur au nombre d’éléments maximal du vecteur. Avec ce registre, on n'a pas besoin d'une seconde boucle pour traiter les éléments restants, et une simple instruction vectorielle peut suffire. L'avantage principal est que cela réduit la portion de code non-parallélisable, donc augmente l'efficience SIMD. Mais l'intérêt n'est pas qu'une question de code. Il faut aussi prendre en compte que cela permet d'utiliser l'ALU plus efficacement. Dès qu'un vecteur court est terminé, le processeur peut immédiatement démarrer le calcul d'un autre vecteur, le vecteur suivant, l'instruction suivante, sans laisser de cycles inutilisés. Avec un processeur SIMD non-vectoriel, on aurait du utiliser de la prédication, donc utiliser seulement une partie des ALU SIMD, ce qui aurait été un gâchis. ===Les accès mémoire=== Les accès mémoire des processeurs vectoriels sont sensiblement les mêmes que ceux des autres processeurs SIMD. Les instructions d'accès mémoire sont les mêmes, on retrouve les modes d'adressages précédents, dont les accès en ''scatter-gather'' et en ''stride''. Sur certains processeurs vectoriels, les instructions vectorielles lisent et écrivent en mémoire RAM, sans passer par des registres : on parle de '''processeurs vectoriels mémoire-mémoire'''. Elles n'avaient pas d'instructions LOAD et STORE, les instructions arithmétiques géraient directement la lecture des opérandes en mémoire, et incrémentaient automatiquement les adresses à lire/écrire. Elles pouvaient gérer des vecteurs de taille arbitraire, la gestion d'un tableau se faisait parfois en une seule instruction ! Le problème est que l'absence de registres pour stocker les données lues réduisait grandement les performances. [[File:Processeur vectoriel mémoire-mémoire.png|centre|vignette|upright=2|Processeur vectoriel mémoire-mémoire]] Les processeurs vectoriels mémoire-mémoire étaient cependant minoritaires, la grosse majorité des processeurs vectoriels récents possèdent des registres vectoriels dédiés aux vecteurs. La plupart sont des architectures de type LOAD-STORE, mais pas toute. De fait, il n'y a pas de lien strict entre adressage des opérandes et processeurs vectoriel (ou SIMT). Seuls les processeurs SIMD de type SWAR ont une restriction là-dessus et sont systématiquement des architectures LOAD-STORE. [[File:Processeur vectoriel à registres vectoriels.png|centre|vignette|upright=2|Processeur vectoriel à registres vectoriels.]] Il faut noter que l'implémentation des accès mémoire est potentiellement différente de celle des autres processeurs SIMD. En principe, on peut utiliser une implémentation identique entre les deux. Mais le fait que les processeurs vectoriels sont pipelinés fait qu'il est préférable d'utiliser une implémentation différente, qui se base sur une RAM ou des caches pipelinés. Vu que l'ALU reçoit une/deux opérandes par cycle, l'idéal est que la mémoire ou le cache aillent au même rythme et soient donc pipelinés.Bien sur, il est possible d'utiliser une implémentation plus classique, avec des caches multiports, un chargement en plusieurs fois des vecteurs depuis le cache, des circuits pour réorganiser les accès mémoire, etc. En théorie, les architectures vectorielles font peu usage de mémoires cache. Les transferts entre registres vectoriels et mémoire RAM sont directs, sans intermédiaire. La raison est que les applications qui tirent parti du SIMD ont une bonne localité spatiale, mais une mauvaise localité temporelle. Or, les caches de données profitent surtout de la localité temporelle pour donner de bonnes performances. De plus, rappelons que les processeurs vectoriels sont assez anciens et datent d'une époque où les transistors étaient précieux. Il valait mieux utiliser des transistors dans un gros cache d'instruction que pour un cache de donnée peu efficace. ==Les systèmes SIMD à plusieurs processeurs : le SIMT== Enfin, terminons avec le dernier type de SIMD, qui date du tout début de l'informatique : celui où on combine plusieurs processeurs non-SIMD. L'idée est de synchroniser plusieurs processeurs de manière à ce qu'ils exécutent la même instruction en même temps, chacun sur des données différentes. On parle d''''exécution ''lockstep''''' des instructions. Toute la difficulté est de garantir la synchronisation des processeurs, garantir que tous les processeurs exécutent la même instruction. Ce qui pose problème quand des branchements sont impliqués, des processeurs pouvant prendre le branchement et pas les autres. ===Les ''array processors'' des temps anciens=== Les premières architectures matérielles conçues pour le parallélisme de données étaient de ce type. On peut par exemple citer l'exemple des Thinking machines CM-1 et CM-2, qui exécutaient tous la même instruction au même cycle d'horloge sur 64000 processeurs minimalistes. Un autre exemple est celui de l'ILLIAC IV. Historiquement, le terme utilisé pour désigner de telles architectures était '''array processors''', encore que ce terme fût réservé aux systèmes multiprocesseurs. Par la suite, le terme ''Single Instruction Multiple Threads'', ou SIMT, a été utilisé. Mais ce terme a ensuite été repris par le marketing de NVIDIA pour désigner une technique précise de SIMD avec prédication, utilisée sur ses cartes graphiques récentes. Le mauvais usage de la terminologie fait que le terme SIMT est devenu polysémique et quelque peu trompeur. L'ILLIAC 4 était un ordinateur un peu particulier, presque unique en son genre. Son architecture était composé d'un processeur principal couplé à plusieurs processeurs secondaires. Il y avait en tout un processeur principal et 64 processeurs secondaires. Le processeur principal chargeait les instructions depuis la mémoire, les décodaient, puis envoyait les instructions aux processeurs secondaires. Le processeur principal était le seul à avoir un ''program counter'', il était utilisé pour gérer les boucles, ainsi que pour configurer les instructions à prédication. Cependant, le processeur principal n'était pas un simple séquenceur : il pouvait exécuter des instructions, avait un accumulateur et des registres, etc. Il prenait en charge toute instruction non-SIMD, alors que les instructions SIMD étaient envoyées aux processeurs secondaires. Les 64 processeurs secondaires avaient chacun : une unité de calcul, un ''local store'' de 2048 flottants de 64 bits, une unité mémoire. Ils n'ont pas de séquenceur ni de ''program counter'', ni de quoi charger une instruction. L'unité de calcul est appelée un ''processing element'' ou PE. Elle intègre un additionneur flottant, de quoi faire des décalages, et une unité de calcul logique. De plus, un PE intègre des registres accumulateurs et de quoi calculer des adresses : un registre d'indice, un registre d'adresse, un circuit de calcul d'adresse. L'unité mémoire connectait l'ALU entière PE et le ''local store'', et elle permettait aussi d'adresser des entrées-sorties. Pour gérer la prédication, les processeurs secondaires pouvaient être activés ou désactivés suivant le résultat des branchements/conditions. Intuitivement, on peut faire le rapprochement avec un processeur SIMD. Le processeur principal regrouperait la partie non-SIMD d'un processeur SIMD, alors que les processeurs secondaires seraient l'unité de calcul SIMD. Cependant, il y a une différence très importante : il n'y a pas de banc de registre vectoriel. À la place, chaque processeur secondaire a son propre banc de registre, et ce sont des registres scalaires ! Une autre différence est que la mémoire RAM est découpée en plusieurs ''local store'' de 2048 adresses, chacun étant relié à une unité de calcul PE. N'oublions pas le système de calcul d'adresse et l'unité LOAD/STORE qui est fusionnée avec l'unité SIMD, chaque PE ayant ses circuits de calcul d'adresse, sa propre unité mémoire, etc. ===Les anciennes cartes graphiques hybrides SIMD-VLIW=== Une technique de SIMD multi-processeur a été utilisée autrefois sur certaines cartes graphiques AMD à architecture Terascale. La différence est que l'architecture en question était multicœur et non multi-processeurs. Elles contenaient plusieurs cœurs VLIW, qui fonctionnaient en ''lockstep''. Un groupe de 16 processeurs VLIWs exécutaient la même opération VLIW. Du moins, c'est ce que semblent dire les [https://www.techpowerup.com/gpu-specs/docs/amd-gcn1-architecture.pdf white papers de présentation de l'architecture GCN]. L'usage de cet hybride SIMD-VLIW était très adapté au traitement graphique. Les cartes graphiques post-années 2000 sont capables d'exécuter des programmes appelés des '''''shaders''''', qui sont utilisés en rendu 3D, pour calculer certaines scènes 3D, et notamment pour calculer les ombres (d'où leur nom de shader, pour shade - ombre). Or, ces shaders sont justement exécutés par la carte graphique, sur les processeurs de shaders. Un processeur de ''shader'' applique un programme/''shader'' sur chaque ''pixel'' ou triangle d'une scène 3D (en fait des fragments et des vertices, mais faisons cette simplification). Chaque triangle ou pixel d'une scène 3D peut être traité indépendamment des autres, ce qui rend le traitement 3D fortement parallèle. Aussi, l'usage du SIMD est assez naturel : chaque processeur exécute un ''shader'' sur un pixel/triangle, utiliser une architecture SIMD parait naturel. Reste à justifier l'usage de plusieurs CPU VLIW. La raison tient dans la manière dont est traité un pixel par un shader. Un pixel est codé en utilisant quatre informations. Premièrement, une couleur codée avec trois nombres flottants, qui représentent une couleur au format RGB avec un mélange de rouge, vert et bleu, chaque couleur rouge/bleu/vert étant codée avec un flottant de 32 bits. La quatrième information est un nombre qui code la transparence du pixel. Généralement, le shader traite différemment la transparence de la couleur RGB, dans le sens où les instructions utilisées ne sont pas les mêmes. Au lieu de traiter couleur RGB et transparence séparément, elles sont traitées en même temps en parallèle, ce qui demande de pouvoir émettre deux instructions : une pour le calcul de la transparence, une autre pour la couleur RGB. Fait amusant, l'instruction utilisée pour gérer la composante RGB est une instruction vectorielle qui travaille sur des vecteurs de 3 couleurs. Les architectures VLIW sont parfaites pour cela. Les cartes graphiques de l'époque de DirectX 9 utilisaient cette méthode qui mélangeait SIMD et VLIW. Elles disposaient de plusieurs cœurs VLIW qui exécutaient le même shader en exécution ''lockstep''. Chaque cœur traitait au moins un pixel à la fois, ou un triangle à la fois. Cependant, nous devons préciser que les cœurs VLIW partagent la même unité de contrôle, ce sont en réalité des chemins de données plus qu'autre chose. L'architecture était aussi combinée avec du ''Fine Grained Multithreading'' afin de pouvoir gérer les accès mémoire, de telles architectures ne pouvant pas se permettre d'utiliser l'exécution dans le désordre. De telles architectures multicœurs à exécution ''lockstep'' permettent quelques optimisations, comme le fait de partager le cache d'instruction entre les cœurs. ==Résumé des différents types de processeurs SIMD== Dans ce chapitre, nous avons appris qu'il existe plusieurs catégories de processeurs SIMD. Les différences entre ces catégories tiennent à la fois dans le jeu d'instruction, mais aussi dans la microarchitecture des processeurs en question. Au niveau du jeu d'instruction, il y a trois paliers, qui sont résumés dans le tableau ci-dessous. {|class="wikitable" |- ! ! Support de la prédication ! Taille des vecteurs ! Instructions horizontales ! Architecture LOAD-STORE ! Pointeur de pile |- ! Processeurs SIMD purs | Non | rowspan="3" | Fixe | rowspan="3" | Limitées à des échanges de données à l'intérieur d'un vecteur, pas d'instructions de réduction | Oui | rowspan="2" | Unique |- ! Processeurs SIMD avec prédication | Oui, par définition | rowspan="3" | Dépend du processeur |- ! Processeurs SIMT | rowspan="2" | Oui, sauf exceptions | Un pointeur de pile possible par élément du vecteur |- ! Processeurs vectoriels | Variable | Toutes supportées, y compris les instructions arithmétiques horizontales | Unique |} Au niveau de la microarchitecture, il y a trois paliers, qui sont résumés dans le tableau ci-dessous. {|class="wikitable" |- ! ! Unité de calcul ! Banc de registre ! Adressage des opérandes |- ! Processeurs SIMD | rowspan="2" | Plusieurs ALU simples non-pipelinée, plusieurs additionneurs/multiplieurs/autres | Banc de registres vectoriels | Architecture LOAD-STORE |- ! Processeurs SIMT | Plusieurs bancs de registres scalaires (non-vectoriels, registres normaux) | rowspan="2" | Dépend du processeur |- ! Processeurs vectoriels | ALU simple unique, pipelinée | Banc de registres vectoriels |} Les processeurs SIMD purs ont des performances différentes des processeurs vectoriels, vu que les premiers font leurs calculs en parallèle, alors que les CPU vectoriels utilisent un pipeline. Mais dans les grandes lignes, les performances sont similaires. Les différences sont grandement atténuées par l'usage du ''vector chaining'', de la gestion des vecteurs de taille variables, et des autres optimisations spécifiques aux processeurs vectoriels. L'avantage est cependant du côté des processeurs SIMD pur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Architectures multithreadées et Hyperthreading | prevText=Architectures multithreadées et Hyperthreading | next=La cohérence des caches | nextText=La cohérence des caches }} {{autocat}} </noinclude> id844pdcw3c2gbvh4idw0ajmhvx7iab 773895 773894 2026-10-01T19:32:26Z Mewtow 31375 /* L'implémentation matérielle */ 773895 wikitext text/x-wiki Nous allons maintenant aborder le parallélisme de données, qui consiste à traiter des données différentes en parallèle. De nombreuses situations s'y prêtent relativement bien : traitement d'image, manipulation de sons, vidéo, rendu 3d, etc. Mais pour exploiter ce parallélisme, il a fallu concevoir des processeurs adaptés. Les architectures les plus simples exécutent une instruction sur plusieurs données en parallèle, à l'intérieur d'un processeur. Ces architectures exploitent le parallélisme de données au niveau de l'unité de calcul, celle-ci pouvant exécuter un même calcul sur des données différentes en parallèle. Si on omet quelques exceptions, on peut classer ces architectures dans plusieurs catégories principales : les processeurs à instructions SIMD, les processeurs à instructions SIMD à prédicat, les processeurs SIMT, les processeurs vectoriels. Si les processeurs vectoriels sont assez rares de nos jours, tous les processeurs récents incorporent des instructions SIMD. Quant au SIMT, il est utilisé sur les cartes graphiques modernes. ==Les instructions SIMD : les points communs entre tous les processeurs SIMD== Les processeurs modernes fournissent presque tous des '''instructions SIMD''', qui sont capables de traiter plusieurs éléments en parallèle. Elles travaillent sur des nombres entiers ou flottants regroupés dans ce qu'on appelle un ''vecteur'', qui sont eux-mêmes stockés dans des registres spécialisés. En général, tous les vecteurs ont une taille fixe, peu importe leur contenu. Cela implique que suivant la taille des données à manipuler, on pourra en placer plus ou moins dans un vecteur. Par exemple, un vecteur de 128 bits pourra contenir 4 entiers de 32 bits, 4 flottants 32 bits, ou 8 entiers de 16 bits. [[File:Vector register.png|centre|vignette|upright=2|Contenu d'un vecteur en fonction du type de données utilisé.]] Les vecteurs sont stockés dans des '''registres vectoriels''', aussi appelés '''registres SIMD'''. Un registre vectoriel peut contenir un vecteur complet, pas plus. En conséquence, ils ont une taille assez importante : ils font généralement 128,,256, voire 512 bits, comparé aux 32/64 bits des registres scalaires. Le résultat est que les processeurs SIMD modernes ont des registres SIMD séparés des registres entiers/flottants normaux. Un défaut de cette organisation est que les registres vectoriels sont des registres en plus, qui doivent être sauvegardés lors des commutations de contexte, lors des interruptions, lors des appels systèmes, etc. {| |+ Comparaison entre un processeur sans registres vectoriels, et avec registres vectoriels. |[[File:Non-SIMD cpu diagram1.svg|vignette|upright=1.5|CPU Non-SIMD]] |[[File:SIMD cpu diagram1.svg|vignette|upright=1.5|CPU SIMD]] |} Les instructions SIMD peuvent être rassemblées en deux grands groupes : les horizontales et les verticales. Les '''instructions SIMD verticales''' travaillent en parallèle sur les éléments qui sont "à la même place" dans deux vecteurs. Elles peuvent additionner ou multiplier deux vecteurs, par exemple. Pour prendre l'exemple d'une instruction d'addition vectorielle, celle-ci va additionner ensemble les données qui sont à la même place dans deux vecteurs, et placer le résultat dans un autre vecteur, à la même place. Les '''instructions SIMD horizontales''' partent d'un vecteur et ont pour résultat un simple nombre. Elles peuvent calculer la somme ou le produit des éléments d'un vecteur, renvoyer le nombre d’éléments nuls dans un vecteur, etc. [[File:Instructions SIMD.png|centre|vignette|upright=2.5|Instructions SIMD]] Les instructions SIMD sont difficilement utilisables dans des langages de haut niveau et c'est donc au compilateur de traduire un programme avec des instructions vectorielles. Les transformations qui permettent de traduire des morceaux de programmes en instructions vectorielles (déroulage de boucles, strip-mining) portent le nom de vectorisation. ===Les instructions SIMD arithmétiques verticales=== Les instructions SIMD sont essentiellement des instructions arithmétiques, comme des additions, des soustractions, des multiplications, éventuellement des opérations bit à bit, et quelques autres. Suivant la taille des données, et le type de celle-si, on devra effecteur des instructions différentes. Par exemple, on devra utiliser deux instructions d'addition différentes suivant qu'on manipule des flottants 64 bits ou des entiers 32 bits. De même, l'instruction pour additionner des vecteurs d'entiers de 16 bits sera différente de celle manipulant des vecteurs d'entiers de 32 bits. L'addition et la multiplication peuvent générer des débordements d'entier. Pour les instructions non-SIMD, le débordement d'une addition est géré par un bit de retenue, stocké dans le registre d'état. Mais pour les instructions SIMD, ce n'est pas une solution très facile à implémenter. À la place, les additions et soustractions utilisent généralement l'arithmétique saturée, à savoir que lorsqu'une addition déborde, le résultat est la valeur maximale représentable dans un entier. Pour la multiplication, les instructions non-SIMD génère un résultat qui est codé sur le double du nombre de bits. Une multiplication de deux nombres 64 bits donnera un résultat sur 128 bits, par exemple. Pour gérer cela, les multiplications non-SIMD stockent ce résultat dans deux registres, ou ne conservent que les 64 bits de poids faible. D'autres solutions sont possibles, mais ces deux là sont les plus utilisées. Pour les instructions SMID, une autre solution est préférée car plus simple pour de telles instructions : fournir deux instructions : une qui calcule les 64 bits de poids faible du résultat, une autre pour les 64 bits de poids fort. Les processeurs SIMD étant utilisés pour du traitement d'image ou du rendu 2D/3D, ils supportent généralement des opérations mathématiques assez complexes sur des nombres flottants. Il n'est pas rare d'avoir des instructions SIMD pour le calcul de l'inverse d'un nombre, de sa racine carrée, l'inverse d'une racine carrée, des fonctions trigonométriques, etc. L'implémentation matérielle de telles instructions est généralement très complexe et gourmande en circuits, ce qui fait que les concepteurs de processeurs rusent. Sauf sur certains processeurs assez peu fréquents, les instructions mathématiques complexes ne calculent pas un résultat exact, mais une approximation du résultat. Utiliser un résultat approximatif ne pose pas de problème pour de telles applications, le rendu d'image ou 3D s'en accommodant parfaitement, contrairement au calcul scientifique. ===Les instructions SIMD de manipulation de données intra-vecteur=== Après avoir vu les instructions SIMD verticales, il est temps de voir les instructions SIMD horizontales. Pour rappel, celles-ci travaillent un vecteur unique, dont elles réarrangent ou modifient les éléments. Tout cela sera plus parlant une fois qu'on aura donné quelques exemples. Mais avant toute chose, nous allons séparer les instructions horizontales en deux sous-types, fondamentalement différents. Le premier sous-type change de place les données dans un vecteur. Le second type est celui des instructions horizontales arithmétiques/logiques habituelles, qu'on verra plus tard. La distinction entre les deux est très importante, comme on le verra plus tard. [[File:SIMD instruction data movement - One operand.png|vignette|upright=1.5|Instructions SIMD de mouvement de données à une opérande.]] Les instructions SIMD horizontales les plus communes bougent des données à l'intérieur des vecteurs, ce qui leur vaut le nom d''''instructions de manipulation de vecteur'''. Les plus simples sont celles qui ont une seule opérande, à savoir qui prennent un vecteur comme opérande et renvoient un vecteur résultat. Les plus intuitives sont les instructions de '''permutation''', dont le nom est assez explicite. Elles changent de place des données dans un vecteur. Dans le cas le plus compliqué, elles prennent deux vecteur : le vecteur dans lequel faire la permutation, et un vecteur qui indique comment faire la permutation. Le vecteur précise, pour chaque élement du vecteur, où il doit aller après la permutation. L'instruction effectue la permutation des entiers/flottants/octets dans le vecteur. Les instructions '''''compress''''' et '''''expand''''' sont elles aussi spécifiques aux vecteurs. L'instruction ''compress'' prend un vecteur en opérande, sélectionne certaines valeurs présentes dedans, et les stocke dans un vecteur de sortie. Elle regroupe les valeurs sélectionnées dans le début du vecteur, et laisse les cases inoccupées à 0. Par exemple, pour un vecteur de 16 entiers, elle permet de sélectionner 5 entiers dedans, et les place dans un vecteur résultat qui ne contient que ces 5 valeurs au début du vecteur, le reste est à 0. L'instruction ''expand'' fait l'inverse : elle prend un vecteur crée par l'instruction ''compress'' (ou du moin qui a le même format), prend toutes les valeurs non-nulles dedans, et les disperse dans un vecteur de sortie, en les mettant à la place désirée. Les autres instructions SIMD horizontales prennent deux opérandes, voire aucune opérande vectorielle (elles prennent un nombre, pas un vecteur). Les instructions '''''shuffle''''' prennent plusieurs vecteurs, sélectionnent les éléments adéquats dans chaque vecteur, et les regroupe dans un vecteur résultat. Par exemple, prenons 2 vecteurs de 16 entiers chacun. Une instruction ''shuffle'' peut prendre 8 entiers dans chaque vecteur et les regrouper dans un vecteur résultat unique de 16 entiers. Ou encore, elle peut prendre trois vecteurs de 16 flottants chacun, prendre 4 flottants dans le premier, 10 dans le second et 2 dans le troisième, et regrouper le tout dans un vecteur résultat de 16 flottants. Il arrive que les deux cas précédents correspondent à des cas séparés, à deux instructions de type ''shuffle'', mais qui fonctionnent différemment. Le premier type prend les N premiers éléments du premier vecteur, puis prend les élements manquant à partir de la fin du vecteur. Le second type prend un élément sur deux dans chaque vecteur, mais avec un décalage d'un rang entre les deux vecteurs. Il y a aussi les instructions dites de '''''broadcast''''', qui copient une valeur unique dans un vecteur entier. Elles sont surtout utilisées pour initialiser des tableaux avec une valeur unique, ou pour remplir un vecteur avec des données prédéterminées pour certains calculs impliquant des constantes. [[File:SIMD instruction exemple.png|centre|vignette|upright=1.5|Instructions SIMD de mouvement de données qui ne sont pas à une opérande.]] ===Les instructions SIMD de réduction=== Il existe des instructions horizontales de type arithmétiques. La plus simple est celle qui additionne les différents entiers/flottants d'un vecteur. Elle est utilisée pour accélérer une opération précise : faire la somme d'un tableau d'entier/flottants. On peut aussi citer les opérations maximum et minimum, qui renvoient le plus grand/petit élément d'un vecteur. Un point important avec ces opérations est qu'elles prennent un vecteur, mais renvoie un résultat unique, un scalaire, un nombre entier/flottant seul. Elles sont parfois appelées des '''instructions de réduction'''. Les instructions de réduction sont généralement la spécificité des processeurs vectoriels, les processeurs SIMD récents n'en ont pas. La raison est que leur implémentation matérielle est généralement assez compliquée. Contrairement aux instructions verticales, elles ont une forme de parallélisme de donnée très limitée. Elles ne travaillent pas exactement sur des données indépendantes, elles font des calculs dont le résultat est utilisé par d'autres, il y a des dépendances de données entre éléments du vecteur. Cependant, il faut savoir que l'on peut émuler une instruction de réduction en utilisant des instructions verticales couplées à des instructions de manipulation de vecteur. Par exemples, prenons le cas d'une instruction de réduction qui additionne entre eux les éléments d'un vecteur. On peut l'émuler en utilisant une addition verticale entre deux vecteurs, et une instruction ''compress''. L'idée est la suivante : on coupe le vecteur initial en deux avec deux instructions ''compress'', ce qui donne deux sous-vecteurs. Puis, on additionne les deux vecteurs résultat entre eux. Puis on répète les deux étapes précédentes jusqu'à obtenir la somme finale. Le fait que l'on puisse émuler les instructions de réduction est la raison principale qui explique que les processeurs SIMD récents n’intègrent pas d'instructions de réduction. Ils se débrouillent avec des instructions SIMD de manipulation de vecteur, et des instructions SIMD verticales. C'est un bon compromis entre cout en circuits et performance. ===Les accès mémoire=== La gestion des accès mémoire est assez hétéroclite, la façon de faire étant différente selon les architectures. Dans le cas le plus simple, les données d'un vecteur sont contigües en mémoire et les instructions ont juste à préciser l'adresse mémoire du début du vecteur, qui est généralement dans un registre d'adresse spécialisé, ou un registre non-SIMD. Avec des vecteurs de 8 octets, toute instruction d'accès mémoire de ce type va lire ou écrire des blocs de 8 octets. L'adresse de départ de ces blocs est soumise à des contraintes d'alignement sur les jeux d'instructions comme le SSE, le MMX, etc. La raison à cela est que gérer des accès non alignés en mémoire rend les circuits de lecture/écriture en mémoire plus complexes. En contrepartie, ces contraintes compliquent l'utilisation des instructions SIMD par le compilateur. D'autres modes d'adressage des vecteurs permettent à une instruction de charger des données dispersées en mémoire pour les rassembler dans un vecteur. On peut notamment citer l'existence d'accès mémoires en stride et en scatter-gather. L''''accès en stride''' regroupe des données séparées par un intervalle régulier d'adresses. Ce mode d'accès a besoin de l'adresse initiale, de celle du premier élément du vecteur, et de la distance entre deux données en mémoire. Il permet aux instructions de mieux gérer les tableaux de structures, ainsi que les tableaux multi-dimensionnels. Lorsqu'on utilise de tels tableaux, il arrive assez souvent que l'on n'accède qu'à des éléments tous séparés par une même distance. Par exemple, si on fait des calculs de géométrie dans l'espace, on peut très bien ne vouloir traiter que les coordonnées sur l'axe des x, sans accès sur l'axe des y ou des z. Les instructions d'accès mémoire en enjambées gèrent de tels cas efficacement. Les processeurs SIMD incorporent aussi les accès en '''''scatter-gather'''''. De tels accès prennent en opérande un vecteur contenant des adresses mémoires, et lit/écrit chacune de ses adresses mémoire indépendamment. Les accès en ''scatter-Gather'' peuvent être vus comme une généralisation de l'adressage indirect à registre aux vecteurs, chaque élément du vecteur étant adressé via adressage indirect. Les accès en ''gather'' sont des lectures : elles prennent un vecteur d'adresse, lisent chaque adresse, et regroupent toutes les données lues dans un vecteur, dans un registre vectoriel. Les accès en ''scatter'' sont l'équivalent pour les écritures. Ils prennent un vecteur d'opérande pour les données à écrire, un vecteur pour les adresses. [[File:Cuda4.png|centre|vignette|upright=2|Adressage en scatter-gather]] Un autre version des accès en ''scatter-gather'' n'utilise pas des adresses mémoire, amis des indices. Le vecteur opérande ne contient pas des adresses, mais des indices qui sont combinés à une adresse de base pour calculer plusieurs adresses. Un tel mode d'adressage permet de faciliter l'implémentation de certaines structures de données, appelées des vecteurs de Liffe. Ils sont très utilisés pour gérer les matrices creuses, des matrices où une grande partie des éléments sont nuls et où, dans un souci d'optimisation, seuls les éléments non nuls de la matrice sont stockés en mémoire. Lors d'une instruction de ''scatter'' ou de ''gather'', tous les accès mémoire n'ont pas lieu en même temps, certains accès mémoire seront terminés avant les autres. Les accès peuvent se faire dans le désordre et de nombreuses optimisations matérielle en profitent pour gagner en performance. Il existe cependant un cas où les accès mémoire doivent se faire dans un ordre bien précis, généralement du premier élément vers le dernier : lorsqu'une instruction ''scatter'' effectue plusieurs écritures à la même adresse. C'est parfaitement possible, et c'est le seul cas où il est intéressant de forcer un ordre pour les écritures. En théorie, une instruction SIMD est un tout, c'est à dire qu'elle ne doit pas modifier les registres SIMD avant que tous les accès mémoire se soient terminés. Cependant, il est possible qu'une partie des accès mémoire déclenchent des défauts de page ou lèvent des exceptions matérielles, alors que les autres se sont terminés. En théorie, le processeur est censé se débarrasser du résultat des accès mémoire terminés. Mais ce serait un gachis de performance, ce qui fait que le processeur viole la règle voulant que les registres SIMD soient mis à jour en une seule fois à la fin de l'instruction de ''scatter-gather''. Dès qu'un accès mémoire se termine, il écrit son résultat dans le registre SIMD, à l'endroit adéquat. Cependant, ce comportement pose un problème. Lorsqu'une exception survient, comme lors d'un défaut de page, le processeur exécute la routine d'interruption associée, puis redémarre l'instruction fautive, le second essai étant le bon. Ici, le processeur ne réexecute pas l'instruction à l'identique, mais seulement les accès non-terminés. Mais comment le processeur sait-il quelles accès mémoire redémarrer ? Il doit pour cela mémoriser quels sont les accès mémoire terminés. Il utilise pour cela un registre architectural appelé le '''masque de complétion'''. Le registre est forcément architectural car il doit être préservé lors d'un changement de contexte, l’exécution de l'exception matérielle forçant un changement de contexte. ===Exemple avec les processeurs x86=== [[File:PD-20060908-SSE3-01.svg|vignette|Chronologie des extensions x86 SIMD.]] Après avoir vu la théorie sur les instructions SIMD, il est temps de voir un exemple concret : celui des instructions SIMD des processeurs x86, présents dans nos PC. Le jeu d'instruction des PC qui fonctionnent sous Windows est appelé le x86. C'est un jeu d'instructions particulièrement ancien, apparu en 1978. Depuis, des '''extensions x86''' ont ajouté des instructions au x86 de base. On peut citer par exemple les extensions MMX, SSE, SSE2, ''3dnow!'', etc. Les premières instructions SIMD furent fournies par une extension x86 du nom de MMX, introduite par Intel en 1996 sur le processeur Pentium MMX. Le MMX a perduré durant quelques années avant d'être remplacé par les extensions SSE. La même année, AMD sorti d'expansion ''3DNow!'', qui ajoutait 21 instructions SIMD similaires à celles du MMX. Celui-ci fût suivi du ''3DNow!+'' quelques années plus tard. Le SSE fût ensuite décliné en plusieurs versions avant d'être "remplacé" par l'AVX. Une grande quantité de ces extensions x86 sont des ajouts d'instructions SIMD. L’'''extension MMX''' ajoutait pas mal d'instructions SIMD assez basiques, essentiellement des instructions arithmétiques : addition, soustraction, opérations logiques, décalages, rotations, mise à zéro d'un registre, etc. La multiplication est aussi supportée, mais avec quelques petites subtilités, via l'instruction PMULLW. Les instructions MMX ne mettent pas le registre d'état à jour et ne préviennent pas en cas d'overflow ou d'underflow si ceux-ci arrivent (pour les instructions qui ne travaillent pas en arithmétique saturée). Le MMX introduisait 8 registres vectoriels, du nom de MM0, MM1, MM2, MM3, MM4, MM5, MM6 et MM7. Ils ne pouvaient contenir que des nombres entiers et faisaient 64 bits. Ils avaient cependant un léger défaut, qui a nui à l'adoption du MMX. Pour rappel, avant le MMX, les flottants étaient gérés par l'extension x87, qui définit 8 registres flottants de 80 bits. Et chaque registre MMX correspondait aux 64 bits de poids faible d'un registre flottant x87 ! Le système d'''alias'' de registres typique des CPU x86 a encore frappé ! En conséquence, il était impossible d'utiliser en même temps l'unité de calcul flottante et l'unité MMX. Par contre, sauvegarder les registres lors d'un changement de contexte, d'une interruption, ou d'un appel de fonction était très simple : la sauvegarde des registres de la FPU x87 suffisait. [[File:MMX-FPU-Register.JPG|centre|vignette|upright=2|Registres MMX et FPU x87.]] [[File:XMM registers.svg|droite|vignette|XMM registers]] Dans les années 1999, une nouvelle extension SIMD fit son apparition sur les processeurs Intel Pentium 3 : le '''Streaming SIMD Extensions''', abrévié SSE. Ce SSE fut ensuite complété, et différentes versions virent le jour : le SSE2, SSE3, SSE4, etc. Cette extension fit apparaitre 8 nouveaux registres, les registres XMM. Sur les processeurs 64 bits, ces registres sont doublés et on en trouve donc 16. En plus de ces registres, on trouve aussi un registre d'état qui permet de contrôler le comportement des instructions SSE : celui contient des bits qui permettront de dire au processeur que les instructions doivent arrondir leurs calculs d'une certaine façon, etc. Ce registre n'est autre que le registre MXCSR. Chose étrange, seuls les 16 premiers bits de ce registre ont une utilité : les concepteurs du SSE ont surement préférés laisser un peu de marge au cas où. La première version du SSE contenait assez peu d'instructions : seulement 70. Le SSE première version ne fournissait que des instructions pouvant manipuler des paquets contenant 4 nombres flottants de 32 bits (simple précision). Je ne vais pas toutes les lister, mais je peux quand-même dire qu'on trouve des instructions arithmétiques de base, avec pas mal d'opérations en plus : permutations, opérations arithmétiques complexes, autres. Petit détail : la multiplication est gérée plus simplement et l'on a pas besoin de s’embêter à faire mumuse avec plusieurs instructions différentes pour faire une simple multiplication comme avec le MMX. On peut quand même signaler une chose : des instructions permettant de contrôler le cache firent leur apparition. On retrouve ainsi des instructions qui permettent d'écrire ou de lire le contenu d'un registre XMM en mémoire sans le copier dans le cache. Ces instructions permettent ainsi de garder le cache propre en évitant de copier inutilement des données dedans. On peut citer par exemple, les instructions MOVNTQ et MOVNTPS du SSE premiére version. On trouve aussi des instructions permettant de charger le contenu d'une portion de mémoire dans le cache, ce qui permet de contrôler son contenu. De telles instructions de prefetch permettent ainsi de charger à l'avance une donnée dont on aura besoin, permettant de supprimer pas mal de cache miss. Le SSE fournissait notamment les instructions PREFETCH0, PREFETCH1, PREFETCH2 et PREFETCHNTA. Autant vous dire qu'utiliser ces instructions peut donner lieu à de sacrés gains si on s'y prend correctement ! Il faut tout de même noter que le SSE n'est pas seul "jeu d'instruction" incorporant des instructions de contrôle du cache : certains jeux d'instruction POWER PC (je pense à l'Altivec) ont aussi cette particularité. Avec le '''SSE2''', de nouvelles instructions furent ajoutés, permettant d'utiliser des nombres de 64, 16 et 8 bits dans chaque vecteur. Le SSE2 incorporait ainsi pas moins de 144 instructions différentes. Ce qui commençait à faire beaucoup. Puis, vient le '''SSE3''', avec ses 13 instructions supplémentaires. Pas grand-chose à signaler, si ce n'est que des instructions permettant d'additionner ou de soustraire tous les éléments d'un paquet SSE ensemble, des instructions pour les nombres complexes, et plus intéressant : les deux instructions MWAIT et MONITOR qui permettent de paralléliser plus facilement des programmes. Le '''SSE4''' fut un peu plus complexe et fut décliné lui-même en 2 versions. Le SSE4.1 introduit ainsi des opérations de calcul de moyenne, de copie conditionnelle de registre (un registre est copié dans un autre si le résultat d'une opération de comparaison précédente est vrai), de calcul de produits scalaire, de calcul du minimum ou du maximum de deux entiers, des calculs d'arrondis, et quelques autres. Avec le SSE4.2, le vice à été poussé jusqu'à incorporer des instructions de traitement de chaines de caractères. [[File:AVX registers.svg|vignette|Registres AVX.]] Avec l''''AVX (Advanced Vector eXtensions)''', on retrouve 16 registres d'une taille de 256 bits, nommés de YMM0 à YMM15 et dédiés aux instructions AVX. Ils sont partagés avec les registres XMM : les 128 bits de poids faible des registres YMM ne sont autres que les registres XMM. L'AVX complète le SSE et ses extensions, en rajoutant quelques instructions, et surtout en permettant de traiter des données de 256 bits. Son principal atout face au SSE est que les instructions AVX permettent de préciser le registre de destination en plus des registres d'opérandes. Avec le SSE et le MMX, le résultat d'une instruction SIMD était écrit dans un des deux registres d'opérande manipulé par l'instruction. Il fallait sauvegarder son contenu si on en avait besoin plus tard, ce qui n'est plus nécessaire avec l'AVX. ==La performance des processeurs SIMD== Il est intéressant de comparer la performance d'un processeur SIMD et celle d'un processeur normal, sans SIMD. Mais cet exercice est compliqué par le fait que les processeurs non-SIMD sont très nombreux : entre les CPU superscalaires, à émission dans l'ordre, exécution dans le désordre, ceux sans rien de tout cela, on a de quoi être perdu. Dans ce qui suit, nous allons comparer deux processeurs basiques sans pipeline : un avec SIMD, un autre sans. Comparé à un processeur sans pipeline, on s'attend à ce que la performance soit augmentée d'un facteur N, avec N le nombre maximal d'entiers/flottants que l'on peut mettre dans un vecteur. Si une architecture SIMD fait N calculs en parallèles, alors les performances sont censées être multipliées par N. Cela laisse penser que plus les vecteurs sont longs, plus le gain en performance est important. Il s'agit là d'un résultat basique, mais la vraie vie est différente. Un premier problème est que ce résultat ne tient que si la mémoire RAM et les caches suivent. En effet, faire N calculs en parallèle demande de lire/écrire N fois plus de données. Les données sont souvent stockées dans des registres vectoriels, ce qui fait que la pression sur le banc de registre est assez importante. Mais cela signifie aussi que les instructions d'accès mémoire doivent lire/écrire N fois plus de données. Il faut alors ajouter des ports sur le cache, élargir le bus mémoire, augmenter la taille des lignes de cache, etc. Le débit binaire des caches et de la mémoire RAM deviennent rapidement des points limitants, qui réduisent le gain en performance des instructions SIMD. Et ne parlons pas de l'interaction avec la mémoire virtuelle : vu qu'on lit/écrit par paquets de données, cela signifie qu'on traverse une page mémoire plus vite, ce qui fait que les défauts de page sont lus fréquents. Un processeur SIMD a intérêt à être combiné à une RAM et des caches solides, très performants. C'est le cas sur les processeurs modernes, mais cela a pose des problèmes pour les processeurs vectoriels, et a mené à leur abandon progressif. Mais passons outre ce problème et regardons la seconde raison qui font que ce résultat est naïf : la loi d'Amdhal ! Toutes les portions d'un programme ne sont pas accélérées par des instructions SIMD, il y a des portions dont les calculs ont des dépendances de données, d'autres qui ne sont pas parallélisables, etc. Une partie du programme est donc impossible à véctoriser (i.e optimiser pour le SIMD), une l'autre l'est. Pour simplifier les explications, on suppose qu'une instruction SIMD prend le même nombre de cycles que son équivalent non-SIMD. Par exemple, une addition SIMD prend le même temps qu'une addition normale. Dans ce cas, la portion non-vectorisable du code reste inchangée, alors que la portion vectorisable est divisée par N, par la largeur du vecteur. On retombe sur une formulation identique à la loi d'Amdhal sur le fond. Une manière de quantifier le tout est d'utiliser la métrique de l'efficience SIMD. Elle est calculée en divisant deux grandeurs. La première est elle-même un rapport : c'est le gain obtenu en terme de nombre d'instructions exécutées. Prenons un programme qui exécute I instructions avant vectorisation, et qui en exécute I/X après. Le gain s'exprime comme suit : : <math>G = \frac{I}{I/X} = X</math> Maintenant, prenons ce gain est divisons-le par la largeur d'un vecteur. On obtient alors l'efficience SIMD : : <math>\text{Efficience SIMD} = \frac{G}{N} = \frac{\frac{I}{I/X}}{N}</math> Notons que le gain et la largeur SIMD ne sont égales que dans un cas bien précis : tout le code est vectorisable. Il s'agit donc d'une reformulation du gain de la loi d'Amdhal, mais dans laquelle le pourcentage de code série est caché. Il est intéressant de regarder l'efficience SIMD quand on augmente la portion du code série, ainsi que la taille des vecteurs. Prenons un cas assez généreux, réaliste pour certaines applications très adaptées au SIMD : 1% du code est non-vectorisable, 99% profite du SIMD. L'efficience SIMD varie alors assez rapidement avec la taille des vecteurs. De 99% pour N=2, elle descend à 98% pour N=16, et tombe sous les 50% pou N=128. La conséquence est que les très grandes tailles de vecteurs ne sont pas vraiment utiles, le rendement est décroissant avec la taille des vecteurs. ==Les processeurs SIMD à registres généraux== Le premier type de processeur SIMD que nous allons voir est celui des processeurs de type '''SWAR''' (''SIMD Within A Register''). Le terme SWAR est un terme polysémique dont le sens a beaucoup changé au fil du temps. Ici, nous allons l'utiliser dans la définition la plus stricte : celle où on effectue du SIMD dans les registres généraux du processeur, sans registres spécialisés. Il s'agit d'une forme de SIMD qui est aujourd'hui peu utilisée, mais qui a été la première forme de SIMD utilisée dans des processeurs grand public commerciaux. L'idée est d'améliorer un petit peu un processeur normal, non-SIMD. Un processeur usuel contient des registres généraux de 32 à 64 bits, qui stockent chacun un opérande de même taille que le registre. D'anciens processeurs avaient des instructions pour effectuer des calculs simples, sur des opérandes plus courts. Par exemple, les processeurs x86 32 bits sont capables de faire des calculs sur 8, 16, ou 32 bits. Mais on ne pouvait placer qu'un seul opérande de 8, 16 ou 32 bits dans un registre général, ce qui est un léger gâchis. Le SWAR est une amélioration de cette technique qui vise à mieux utiliser les registres et l'ALU. L'idée est de regrouper plusieurs opérandes de 8, 16, voire 32 bits, dans un seul registre général. De plus, on ajoute des instructions SIMD d'addition/soustraction/autres, qui lisent des opérandes 8/16/32 bits dans ces registres généraux. Par exemple, sur un processeur 32 bits, on peut ajouter une opération d'addition qui lit deux registres de 32 bits, pour récupérer quatre opérandes de 16 bits (deux par registre) et effectue deux additions 16 bits en même temps dans l'ALU. Et il y a la même chose pour d'autres opérations, comme la soustraction, la comparaison, etc. Les instructions SIMD disponibles sur ces processeurs se limitent généralement à des additions, soustractions, comparaisons, mais guère plus. De plus, il s'agit d'instructions entières, il n'y a pas d'instructions SIMD flottantes. La raison à cela est très simple, mais nous l'expliquerons plus bas, dans la section sur l'implémentation. Disons simplement que de tells architectures visent une économie en circuit maximale. Elles implémentent les instructions SIMD en utilisant le moins de circuits possibles, ce qui fait qu'elles réutilisent les registres généraux et n'ont pas de registres SIMD séparés. ===Les extensions/jeux d'instructions de type SWAR=== Les premiers processeurs à intégrer du SWAR étaient les processeurs DEC Alpha, avec l'extension multimédia '''''Motion Video Extensions'''''. Les processeurs en question étaient les processeurs Alpha 21164PC (PCA56 and PCA57), Alpha 21264 (EV6) and Alpha 21364 (EV7). Les instructions SIMD disponibles étaient très simples et se résumaient à quelques comparaisons : trouver le maximum ou le minimum de deux opérandes de 8/16 bits, quelques instructions de permutation. Les extensions '''''Multimedia Acceleration eXtensions''''' des processeurs Hewlett-Packard PA-RISC sont aussi de ce type, mais disposent de plus d'instructions SIMD. La première version, appelée MAX-1, était disponible sur les processeurs 32 bits de la marque, à partir du processeur PA-7100LC. Les instructions disponibles étaient des additions et soustractions d'opérandes 16 bit. Il y avait en tout trois instructions d'addition et trois pour la soustraction. En tout, il y avait : * une instruction d'addition/soustraction non-signée utilisant l'arithmétique modulaire ; * une instruction d'addition/soustraction signée en arithmétique modulaire ; * une instruction d'addition/soustraction signée en arithmétique saturée. Outre les additions/soustractions, il y avait aussi une instruction pour calculer la moyenne de deux opérandes 16 bits, ainsi qu'une addition fusionnée avec un décalage. La seconde version, appelée MAX-2, ajouta des instructions d'additions fusionnées avec des décalages, mais aussi des instructions de permutation. De plus, cette version était disponible sur les processeurs 64 bits de la marque, ce qui fait que les registres étaient eux aussi de 64 bits. Ils pouvaient contenir deux fois plus d'opérandes 16 bits. Il n'y avait de gestion d'opérandes 32 bits pour les extensions SIMD. ===L'implémentation matérielle=== L'avantage de cette technique est la grande simplicité d'implémentation. On n'ajoute pas de registres SIMD séparés, ce qui a de nombreux avantages : économie de circuits car pas besoin d'un second banc de registre, pas besoin de sauvegarder des registres SIMD en plus lors d'un appel système/interruption, etc. L'implémentation a juste besoin de modifier le décodeur et l'unité de calcul. Le décodeur est modifié pour ajouter des instructions en plus, rien de spécifique au SWAR. Les modifications de l'unité de calcul sont spécifiques au SWAR et modifient la manière dont elle gère les retenues. Une première solution est d'utiliser une ALU bit-slicée, ce qui est l'idéal pour les unités de calcul entières. Par exemple, pour une ALU 32 bits entière, on peut la découper en 4 unités de calcul 8 bits. Pour une opération SIMD avec 4 opérandes 8 bits, les quatre ALU fonctionnent en parallèle, il n'y a pas transmission des retenues. Pour les calculs SIMD avec des opérandes de 16 bits, on regroupe les ALU par paire et on propage les retenues à l'intérieur d'une paire, mais pas entre les paires. Enfin, pour un calcul non-SIMD, les opérandes de 32 bits, on propage les retenues normalement, d'une ALU 8 bits vers la suivante. Une autre solution prend un additionneur 32/64 bits normal, mais mets à 0 les retenues. L'implémentation est très simple avec un additionneur à anticipation de retenues, où les retenues sont calculées en avance, avant de faire l'addition proprement dite, par un circuit d'anticipation de retenue. Il suffit alors d'ajouter un circuit de masquage en sortie du circuit d'anticipation de retenue, qui met à zéro les retenues adéquates. La gestion des débordements demande d'ajouter des circuits, ou du moins de modifier ceux déjà présents dans l'ALU. Typiquement, les circuits pour gérer l'arithmétique saturée sont un peu modifiés pour effectuer la mise à 1111...111 octet par octet et chaque octet est masqué si besoin. [[File:ALU modifiée pour implémenter du SWAR.png|centre|vignette|upright=3|ALU modifiée pour implémenter du SWAR]] Notons que la technique ne s'applique qu'aux additions, et aux opérations dérivées comme la soustraction et les comparaisons (ces dernières sont des soustractions déguisées). Mais les multiplications ou divisions ne peuvent pas s'implémenter simplement en modifiant des ALU existantes, du moins pas avec des modifications simples. Aussi, les processeurs qui utilisent cette technique se bornent aux additions et opérations dérivées, pas plus. La gestion des opérations de permutation est quant à elle très simple : beaucoup de processeurs disposent déjà d'instructions de permutation d'octets, qui sont l'équivalent d'instructions SIMD de permutation pour les registres généraux. Nous en avions déjà parlé dans le chapitre sur le langage machine et l'assembleur, quand nous avions fait la liste des instructions les plus courantes. De telles instructions de permutation d'octet sont utiles pour gérer le boutisme ou effectuer quelques manipulations assez rares. Pas besoin de rajouter une ALU dédiée, celle-ci est déjà présente de base, du moins si les instructions de permutation d'octet sont déjà présentes. ==Les processeurs SIMD purs (''packed SIMD'')== Maintenant que nous avons vu les instructions SIMD, passons maintenant aux processeurs eux-même. Tous les processeurs que nous allons voir dans ce qui suit supportent des instructions SIMD. Mais leur implémentation matérielle, leur micro-architecture, n'est pas la même. Ils ont aussi quelques différences en termes de jeu d'instruction, mais qui sont fortement liées à l'implémentation matérielle. Nous allons ici voir les '''processeurs SIMD purs''', aussi dits de type ''Packed SIMD''. Ils implémentent des instructions SIMD sans rien de plus, juste le strict minimum : pas de prédication, pas de vecteurs de taille variable, presque pas d’instructions horizontales, architecture de type LOAD-STORE. De tels processeurs utilisent plusieurs ALU entières/flottantes qui travaillent en parallèle. De plus, ils disposent de registres SIMD séparés des registres généraux. Voyons ces points immédiatement. * La première distinction est que les vecteurs sont stockés dans des registres séparés des registres généraux, appelés les '''registres SIMD''', ou ''registres vectoriels''. Ils sont beaucoup plus larges que les registres classiques et mémorisent 2, 4, 8 entiers/flottants. * La seconde distinction est que les calculs SIMD sont effectués dans une unité SIMD séparée de l'ALU ou de la FPU. L'unité de calcul SIMD peut exécuter au minimum des instructions SIMD verticales, plus rarement des instructions SIMD horizontales. Pour ce faire, elle regroupe plusieurs additionneurs/multiplieurs, qui effectuent des calculs en parallèle. ===Les instructions SIMD verticales : plusieurs ALU travaillant en parallèle=== [[File:SIMD2.svg|vignette|Parallélisme de données au niveau de l'unité de calcul. Celle-ci contient plusieurs circuits indépendants qui appliquent la même opération sur des données différentes.]] Une unité de calcul SIMD gère au minimum des instructions SIMD verticales. Et pour ce faire, elle contient plusieurs additionneurs/multiplieurs séparés. Par exemple, pour additionner deux vecteurs contenant chacun 16 entiers/flottants, il faut utilise 16 additionneurs entiers et 16 additionneurs flottants. Dans le cas général, une ALU SIMD est composée de plusieurs ALU entières et flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. Notons que les ALU travaillent en parallèle, elles font des calculs indépendants. Notons que les additionneurs dans chaque ALU doivent pouvoir être configurés de manière à gérer des tailles de données différentes. Par exemple, si on prend un vecteur simple, qui peut contenir soit 32 entiers de 16 bits, soit 16 entiers 32 bits, soit 8 entiers 64 bits, alors on doit utiliser 8 additionneurs, mais chacun d'entre eux doit pouvoir être reconfiguré de manière à ne pas propager les retenues au-delà des premiers 16 ou 32 bits. Un défaut de cette organisation est que le cout en circuits est loin d'être négligeable. Il faut dupliquer des unités de calcul et les coller en rajoutant des circuits, cela utilise beaucoup de transistors. Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures. Retenez cependant : l'usage de plusieurs ALU travaillant en parallèle a plusieurs défauts. Premièrement, cela implique des vecteurs de taille fixe, du fait que le nombre d'ALU travaillant en parallèle est fixe. Deuxièmement, des difficultés d'implémentation concernant les instructions de réduction. Détaillons ce second point ! Une autre possibilité est d'utiliser moins d'unités de calcul qu'il n'y a d'éléments dans un vecteur. Par exemple, pour des vecteurs contenant 32 flottants, il se peut qu'il n'y ait que 16 ALU. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale. L'avantage est que l'ensemble des calculs se fait en une seule instruction machine. L'implémentation est généralement la suivante : le processeur décode une instruction SIMD en deux micro-instructions SIMD plus courtes. Par exemple, pour une instruction SIMD de 32 éléments, elle sera décodée en deux instructions SIMD de 16 éléments, chacune étant exécutée sur l'ALU. Il est possible de réduire fortement l'impact en performance en doublant la fréquence de l'ALU. Par exemple, pour des vecteurs contenant 32 flottants, le processeur incorpore 16 ALU, mais celles-ci fonctionne à une fréquence double de celle du processeur. L'usage d'une ALU à double fréquence est une technique qui a été utilisée sur le Pentium 4 et quelques processeurs, pour des instructions non-SIMD. Mais pour les processeurs SIMD, elle s'applique à la perfection. L'implémentation est la même que précédemment, sauf que le ''scheduler'' fonctionne à double fréquence. Pour limiter la casse, il est préférable d'utiliser un ''scheduler'' séparé pour les instructions SIMD, qui sera seule à fonctionner à double fréquence. ===Les instructions horizontales : une ALU simple séparée=== Les instructions horizontales posent des difficultés d’implémentation. Elles ne peuvent pas s'implémenter en utilisant plusieurs ALU travaillant en parallèle, contrairement aux instructions verticales, ce qui fait qu'elles utilisent généralement des unités de calcul spécialisées. Et c'est là que la différence entre instructions de manipulation de vecteurs et instructions de réduction vient encore une fois poser problème. Les instructions de manipulation de vecteur sont relativement simples à implémenter : elles ont juste besoin d'une ALU spécialisée, relativement simple, composée de beaucoup de multiplexeurs à configurer convenablement. Mais pour les instructions de réduction, c'est autre chose ! L'implémentation des instructions de réduction est possible mais a un cout en circuits assez prohibitif. Pour additionner N entiers, il faut utiliser un additionneur multi-opérande qui prend beaucoup de circuits et dont le temps de calcul est proche de celui d'une multiplication (très lent). Trouver le maximum et le minimum d'un nombre demandent de faire la même chose avec des comparateurs. Le tout a un cout en circuit non-négligeable, pour accélérer des opérations assez peu fréquentes. Elles sont surtout utilisées pour trouver la somme ou le maximum/minimum d'un tableau, opération séquentielle par nature, avec peu parallélisme de données, peu courante. Une autre solution utilise une unité SIMD normale, mais intègre un système de contournement, de ''bypass'', qui relierait l'entrée d'une ALU à la sortie d'une autre. Mais le cout lié aux interconnexions est très important : on doit relier chaque ALU à toutes les autres dans le pire des cas, on peut optimiser le tout et réduire un peu la quantité d'interconnexions, mais le cout reste important. Au final, on gagne en circuits ce qu'on perd en interconnexions. Le choix entre les deux solutions est loin d'être facile et dépend du processeur, de la technologie utilisée, du budget en transistors disponibles, etc. Les processeurs SIMD purs résolvent ce problème assez simplement : ils n'implémentent pas d'instructions de réduction. Par contre, ils implémentent systématiquement des instructions de manipulation de vecteur, comme des permutations ou autres. La raison est que l'on peut émuler les instructions de réduction en utilisant des instructions SIMD de manipulation de vecteur couplées à des additions/multiplications SIMD verticales. Il s'agit donc d'un compromis entre cout en circuits et performance finale. Niveau circuits, on a une unité SIMD avec plusieurs ALU de calcul en parallèle, une unité de manipulation de vecteur séparée assez simple et au cout en circuits raisonnable. Les performances pour les opérations de réduction sont acceptables, le cout en performance est relativement modéré. ===Le banc de registre vectoriel=== La présence de registres vectoriels séparés permet d'avoir des vecteurs assez longs. Pour ce faire, les registres vectoriels sont plus grands que les registres généraux. Par exemple, sur un processeur 32 bits, qui gère donc des entiers de 32 bits, les vecteurs peuvent faire 128 ou 256 bits. Un vecteur contient donc plusieurs entiers ou flottants de taille maximale. On n'est pas dans le cas précédent, où registres généraux et SIMD sont les mêmes, ils sont séparés et n'ont pas la même taille. Le processeur utilise un banc de registre séparé pour les registres SIMD, appelé le '''banc de registre SIMD'''. Vu que les registres vectoriels sont assez longs, le banc de registre vectoriels a une grande taille, une grande capacité mémoire. Pour donner un exemple, les extensions SSE des processeurs x86 définissent 8 registres SIMD de 128 bits chacun. Ils ont été introduits à une époque où les processeurs avaient des registres généraux de 32 bits. Le banc de registres associé était donc environ 4 fois plus grand que celui pour les registres généraux. Je dis environ car le décodeur avait sensiblement la même taille, seul le plan mémoire était grossit, mais l'idée est là. Une conséquence est que le banc de registres SIMD consomme beaucoup d'énergie et d'électricité. Il en est de même avec les ALU SIMD. Et diverses optimisations tentent de réduire ce cout en énergie. L'une d'entre ajoute une mémoire cache entre les ALU SIMD et les registres vectoriels ! L'idée est que si un registre vectoriel est relu plusieurs fois de suite, la première lecture lira le vecteur dans le banc de registres SIMD, mais les lectures suivantes liront le vecteur dans le cache. L'optimisation marche car accéder au banc de registre vectoriel est vraiment couteux, plus qu'accéder à un cache. Le cache en question est appelé un '''''register reuse caches'''''. Les ''register reuse caches'' sont présents sur le processeur Monaka de Fujitsu, mais aussi et surtout dans les cartes graphiques modernes. Nous ferrons l'impasse sur les GPU modernes, car leur usage est quelque peu particulier. Il ne servent pas que à réduire la consommation d'énergie, mais aussi à réduire les conflits d'accès au banc de registre, qui est en réalité composé de plusieurs banques indépendantes. Leur usage sur le CPU Monaka est plus simple : ils ne servent vraiment qu'à réduire la consommation d'énergie, rien de plus. : Pour ceux qui veulent en savoir plus pour leur utilisation sur les cartes graphiques, tout est détaillé dans mon wikilivre sur les cartes graphiques, via le lien suivant. Mais sachez cependant qu'il faut idéalement lire le chapitre entier, et peut-être même les chapitres précédents, pour comprendre cette section. Voici le lien : [https://fr.wikibooks.org/wiki/Les_cartes_graphiques/La_microarchitecture_des_processeurs_de_shaders#L'Operand_Collector_et_les_caches_de_register_reuse L'Operand_Collector_et_les_caches_de_register_reuse] ===L'implémentation des LOAD-STORE à des données consécutives=== Les architectures SIMD pures sont des architectures de type LOAD-STORE, ce qui veut dire que les instructions SIMD ne peuvent que lire ou écrire dans les registres vectoriels, pas en mémoire ou ailleurs. Les transferts entre registres SIMD et mémoire sont le fait d'instructions LOAD et STORE spécialisées, qui transfèrent des vecteurs entre RAM et registres vectoriels. Les autres architectures n'ont pas nécessairement ce genre de restrictions, mais laissons cela pour plus tard. Implémenter les instructions LOAD/STORE pour des vecteurs dont les données sont consécutives en mémoire n'est pas très compliqué. Il suffit juste d'élargir le port de lecture/écriture du cache L1 de données, pour pouvoir lire/écrire un vecteur entier. Rien de bien compliqué, il suffit juste d'ajouter des interconnexions et d'ajouter des multiplexeurs. Du moins, c'est le cas tant que les vecteurs sont plus petits qu'une ligne de cache, ce qui est souvent le cas en pratique. Dans le cas très rare où les vecteurs sont plus longs qu'une ligne de cache, les LOAD/STORE doivent se faire en deux accès dans le cache, ce qui demande d'ajouter du matériel pour contrôler la situation. Précisons cependant qu'il s'agit là d'un cas idéal, où les vecteurs sont correctement alignés en mémoire. En effet, il se peut qu'un vecteur soit à cheval sur deux lignes de cache, alors qu'il est plus petit qu'une ligne de cache. Il suffit pour cela qu'il soit placé à une adresse adéquate. Par exemple, prenons un vecteur de 16 octets et des lignes de cache de 32 octets. Si le vecteur démarre à l'adresse 24, alors il sera à cheval sur deux lignes de cache. Il faut donc intégrer des circuits pour gérer cette situation. Pour cela, il suffit d'ajouter un registre pour stocker la première de ligne lue, puis un circuit qui combine les deux lignes de cache pour donner le vecteur final. ===L'implémentation des LOAD-STORE en ''scatter-gather''=== L'implémentation des accès en ''scatter-gather'' est plus compliquée. Un accès en ''scatter-gather'' demande de faire des calculs d'adresse, suivis par un ensemble de lectures/écritures, suivis par un regroupement des données lues dans un vecteur. Le calcul d'adresse peut se faire en parallèle dans une unité de calcul SIMD, dans plusieurs unités de calcul d'adresse en parallèle. Par contre, les lectures/écritures ne le peuvent pas. Le cache n'a pas assez de ports de lecture/écriture pour lire/écrire N données. Il a quelques ports, ce qui lui permet de lire/écrire 3/4 données grand maximum, soit moins que ce qui est nécessaire pour faire un accès en ''scatter-gather'' en une fois. Les lectures/écritures sont donc effectuées en plusieurs fois. Pour une opération de ''Gather'', il faut aussi regrouper les données lues dans un vecteur. Sur les caches des processeurs à haute performance, le calcul d'adresse est intégré dans le décodeur. Ce sont des caches adressés par somme, que nous avions abordé dans le chapitre sur les mémoires caches. Avec de telles caches, on ne peut pas utiliser une unité de calcul d'adresse SIMD, on ne peut calculer qu'autant d'adresse qu'il y a de ports sur le cache. Les accès en ''scatter-gather'' regroupent plusieurs accès mémoire, qui peuvent être effectués dans le désordre. La seule exception est celle où un ''scatter'' effectue plusieurs écritures à la même adresse, où les deux écritures doivent se faire dans l'ordre allant du premier au dernier élément du vecteur d'adresse. Les processeurs SIMD profitent de cette absence d'ordre pour effectuer les accès dans un ordre le plus optimisé possible. Par exemple, imaginons le cas où une instruction ''gather'' lise des éléments placés dans deux lignes de cache uniquement, mais dans le désordre en passant sans cesse d'une ligne de cache à l'autre. Il est alors possible de faire tous les accès dans la première ligne de cache en premier, avant de faire ceux allant dans l'autre. Une telle optimisation marche aussi bien pour les lectures que les écritures, pour les ''gather'' que pour les ''scatter''. Elle peut aussi être adaptée pour plus de deux lignes de cache. Elle porte le nom de '''''memory coalescing'''''. Une autre optimisation possible est de détecter le cas où un accès en ''scatter-gather'' accède à des données consécutives. Il suffit pour cela que les indices/adresses du vecteur opérande soient consécutives, et cela arrive plus souvent qu'on ne le pense. Dans ce cas, l'accès est remplacé par une instruction LOAD/STORE SIMD normale, beaucoup plus simple et plus rapide. Les deux optimisations demandent que les adresses soient calculées avant de faire l'accès mémoire. Les adresses calculées sont alors comparées entre elles, par un réseau de comparateurs assez complexe. Un point important de l'implémentation des instructions ''gather'' est qu'une donnée chargée doit être insérée au bon endroit dans le vecteur destination. Et pareil pour les ''scatter'' : il faut lire la bonne donnée au bon moment. Si on prend une ligne de cache complète, il faut lire plusieurs élèments en même temps et les envoyer au registre de destination au bon endroit. Il faut pour cela tout un réseau de multiplexeurs assez complexe pour faire ce travail. Il y a le même genre de réseau pour les instructions ''scatter'', mais qui va dans l'autre sens : du registre vers la ligne de cache. ==Les processeurs SIMD avec prédication== Un obstacle très gênant à la vectorisation est la présence de branchements conditionnels dans les boucles à vectoriser. Si une boucle contient des branchements conditionnels, elle ne peut pas être vectorisée facilement : il est impossible de zapper certains éléments d'un vecteur suivant une condition. Il n'est pas possible d'effectuer une instruction SIMD seulement sur certains éléments d'un vecteur et d'ignorer les autres, en fonction du résultat d'un branchement conditionnel. Tout le vecteur y passe, ou le vecteur est épargné, mais la condition intermédiaire n'est pas possible avec du SIMD simple. Pour résoudre ce problème, les processeurs SIMD incorporent diverses techniques pour implémenter ou éviter les branchements conditionnels le plus possible. La plus simple est l'incorporation de certaines instructions SIMD simples, comme la valeur absolue, le calcul du minimum/maximum, etc. Ces opérations sont généralement émulées en utilisant des branchements, avec quelques conditions simples. Incorporer ces instructions permet ainsi de faire disparaitre une partie des branchements du code. Elle ne paye pas de mine, mais elle a un résultat pas négligeable pour le rendu graphique, certaines applications de calcul scientifiques, et quelques autres. Son cout en transistors est très variable, il faut rajouter des circuits dans le séquenceur, l'unité de calcul SIMD est assez peu modifiée (il faut juste rajouter des multiplexeurs). Le vrai problème est que cette solution laisse énormément de branchements dans le code. Et qu'il faut donc trouver une solution plus générale, capable de réduire drastiquement les branchements. Pour cela, on réutilise une technique vue dans les chapitre sur l'assembleur et le langage machine : la prédication. ===Les instructions à prédicats=== Pour optimiser le cas général, il est possible d'utiliser des instructions à prédicats adaptées pour fonctionner sur des vecteurs. L'idée est que quand une instruction effectue un calcul sur un ou deux vecteurs, certains éléments du vecteurs sont ignorés. Les éléments à ignorer sont choisis suivant le résultat d'une instruction de comparaison, qui effectue un test : les éléments pour lesquels ce test est respecté sont pris en compte, ceux qui ne passent pas le test sont ignorés. Pour donner un exemple d'utilisation, imaginons que l'on ait un vecteur dans lequel on veut remplacer toutes les valeurs négatives par des 0. Dans ce cas, on utilise : * une instruction de comparaison, qui compare chaque élément du vecteur avec 0 et génère plusieurs bits de résultat ; * suivi d'une instruction à prédicat qui met à zéro les éléments pour lesquels les bits de résultat précédents sont à 1. La prédication peut aussi être utilisée pour simuler des vecteurs de taille variable. Tous les vecteurs ont une taille fixe, mais on peut ne pas faire de calculs sur la fin d'un vecteur, ce qui fait que tout se passe comme si le vecteur était plus petit. Ce n'est pas sa seule utilisation, mais la prédication permet de simuler ce comportement. Néanmoins, cela n'en fait pas un processeur vectoriel, capable de traiter des vecteurs de taille variable : il n'y a pas de moyen de préciser explicitement la taille des vecteurs à traiter. Tout cela demande plusieurs modifications du jeu d'instruction : ajouter des instructions de comparaison SIMD, ajouter de quoi faire la prédication. ===Le calcul des masques et le registre de masque=== La prédication demande que le résultat d'une comparaison soit stocké dans un registre à prédicat ou dans le registre d'état. Pour les processeurs SIMD, il y a cependant une grosse adaptation à faire concernant le fonctionnement des comparaisons et le stockage des résultats. Premièrement, les instructions de comparaison SIMD comparent une paire de deux éléments à la même place dans deux vecteurs et fournissent un résultat d'un bit pour chaque paire. Le résultat est donc un ensemble de bits, qui sont regroupées dans un vecteur spécialisé. Deuxièmement, reste à savoir que faire de ce vecteur. Pour cela, deux solutions sont possibles. Avec la première, le vecteur est stocké dans un registre vectoriel/SIMD comme les autres, il est traité comme n'importe quel autre vecteur. Avec la seconde solution, il reçoit un registre dédié, qui est un mix entre registre d'état et registre à prédicat, appelé un '''registre de masque''' (''Vector Mask Register''). Il stocke un bit pour chaque donnée présente dans le vecteur à traiter, qui indique s'il faut ignorer la donnée ou non. L'instruction SIMD suivante fait usage de ce masque pour savoir s'il faut faire l'opération pour chaque élément du vecteur. [[File:Vector mask register.png|centre|vignette|upright=2|Vector mask register]] Il peut y avoir un ou plusieurs registres de masque. S'il y en a plusieurs, ils sont généralement nommés, sur le modèle des registres à prédicats. L'application d'un masque demande de fournir le nom du registre de masque à utiliser, ils adressés explicitement dans les instructions. S'il n'y en a qu'un seul, il est possible de le pas le nommer et de faire en sorte qu'il soit adressé implicitement par les instructions qui en ont besoin. On parle alors de '''masquage implicite'''. Le masquage implicite est très utilisé sur les cartes graphiques, qui sont actuellement des architectures SIMD, comme nous le verrons plus bas. Un gros problème du masquage implicite est qu'il introduit des dépendances implicites entre instructions, qui ne sont pas explicites quand on regarder uniquement les registres manipulés par une instruction, qui perturbent le renommage de registres. Rien d'insurmontable, mais cela peut poser des problèmes de performance et la résolution de ce problème a un cout en hardware. Mais ce n'est un problème que sur les architectures à exécution dans le désordre. Les architectures à émission dans l'ordre ne sont pas concernées, et c'est notamment le cas sur les GPU et les cartes graphiques modernes. Diverses optimisations sont possible avec un registre de masque. La première est de ne pas exécuter d'instruction si tous les bits du masque sont à 0. Dans ce cas, l'instruction SIMD n'a pas de calculs à faire, vu que tous les résultats sont masqués. Il est alors possible de ne pas exécuter l'instruction SIMD et de passer directement à la suivante. L'implémentation matérielle est très simple : une porte NOR qui détermine si le registre de masque contient la valeur 0. La sortie de cette porte NOR est envoyée au séquenceur d'instruction. Le décodeur est alors conçu pour zapper l'instruction si le registre de masque contient un zéro. ===Les accès mémoire en ''scatter-gather'' avec prédication=== Un problème avec les accès mémoire que certains accès peuvent déclencher des défauts de page ou d'autres formes d'exceptions (erreurs de protection mémoire, autre). Si l'accès mémoire se fait sans prédication, ces exceptions sont gérées dans l'ordre des accès mémoire, en commençant par le début du vecteur, ce qui rend l'implémentation assez facile. Mais avec la prédiction, il se peut qu'un accès masqué et censé être ignoré déclenche une exception matérielle. Et le résultat dépend de l'implémentation. Une première solution est d'effectuer l'accès mémoire, et de masquer ensuite les éléments à ignorer. Mais dans ce cas, les éléments masqués peuvent déclencher des exceptions, qui ne sont pas censées se produire. Une autre solution est de déterminer quelles sont les adresses qu'il faut lire ou écrire et ne pas faire les accès mémoire masqués. Une autre solution effectue tous les accès avant de savoir quels sont ceux à masquer, mais de ne pas déclencher d'exception avant qu'on sache quels sont les accès masqués. Une fois le masquage des accès effectués, le processeur exécute les exceptions adéquates. Il s'agit d'un comportement de masquage d'exception, très utilisé. L'implémentation est assez simple pour les accès mémoire contiguës, mais plus compliqué pour les accès en ''stride'' ou en ''scatter-gather''. Pour les accès contiguës, les seules exceptions pouvant survenir sont celles ayant lieu quand on passe d'une page mémoire à la suivante (au sens pagination, mémoire virtuelle). Et le hardware pour détecter un débordement de page est assez simple. Mais pour les accès en ''stride'' ou en ''scatter-gather'' demandent qu'oin vérifie les exceptions pour chaque élément du vecteur. ===L'implémentation matérielle=== L'implémentation matérielle est assez proche des processeurs SIMD purs. On retrouve plusieurs unités de calcul travaillant en parallèle, avec quelques circuits ajoutés pour gérer la prédication. Le cout supplémentaire en circuits est acceptable, les gains en performance associés sont très intéressants. Les circuits en plus, pour gérer la prédication, sont similaires à ceux utilisés pour la prédication sur les architectures non-SIMD, mais sont dupliqués. La seule innovation architecturale est l'implémentation du registre de masque. Tous les processeurs n'en utilisent pas, mais c'est la solution la plus performante. En effet, ce registre de masque est un peu l'équivalent du registre d'état pour les instructions SIMD, bien qu'il soit techniquement une concaténation de registres à prédicat non-nommés. Ils ont pour avantage de libérer des registres SIMD normaux tout en ayant un cout en matériel très limité. Un autre point est que pour gérer les masques, les instructions SIMD à prédicat doivent lire le masque. Sans registre de masque, en utilisant un registre SIMD normal, il faut rajouter un troisième port de lecture sur le banc de registre pour récupérer le masque, ce qui est couteux en matériel. Pas besoin avec un registre de masque, qui n'est connecté qu'à l'ALU, sur le même modèle que le registre d'état. Plus haut, on a vu que l'implémentation de l'ALU SIMD peut se faire en utilisant des moins d'ALU que prévu. Par exemple, pour des vecteurs de 32 éléments, on peut utiliser 16 ALUs (allant à double fréquence ou non). Une instruction SIMD est alors décodée en plusieurs micro-instructions SIMD consécutives, chacune traitant une partie d'un vecteur. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. L'avantage est que cela se marie bien avec l'abandon des opérations pour les masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit vérifier que les 8 bits de poids faible ainsi que de poids fort du registre de masque ne soient pas individuellement à zéro. Cela se fait en dupliquant la porte NOR évoquer plus haut, l'une pour la partie basse du registre et la seconde pour la partie haute. ==La prédication pour les structures de contrôle imbriquées== Au niveau du jeu d’instruction, les architectures SIMT implémentent de la prédication, sous une forme améliorée. Les processeurs SIMT actuels sont surtout utilisées sur les processeurs intégrés aux cartes graphiques. Et ces derniers gèrent très mal les branchements, et encore : beaucoup de cartes graphiques, même récentes, ne gèrent tout simplement pas les branchements. Elles doivent donc se débrouiller avec uniquement la prédication, là où les processeurs SIMD utilisent des branchements normaux en complément de la prédication. Insistons sur le fait que cet usage exclusif de la prédication n'est présent que sur une sous-partie des architectures SIMT, le seul exemple que l'auteur de ce wikilivre connait étant celui des cartes graphiques. Les architectures SIMT sans branchements doivent donc trouver des solutions pour gérer les structures de contrôle imbriquées, à savoir une boucle placée à l'intérieur d'une autre boucle, un IF...ELSE dans un autre IF...ELSE, etc. Elles utilisent pour cela la prédication, combinée avec des mécanismes annexes. Le premier d'entre eux est l'usage de plusieurs registres de masques organisés d'une manière bien précise, l'autre est l'usage de compteurs d'activité. Voyons ces deux techniques. ===La pile de masques=== La '''pile de masques''' remplace le ou les registres de masque. Sans elle, le processeur SIMD incorpore un registre de masque qui est adressé implicitement ou explicitement. Éventuellement, le processeur peut contenir plusieurs registres de masque séparés adressables via un nom de registre. Avec elle, le processeur SIMD incorpore plusieurs registres de masque organisé en pile. Le registre de masque est donc remplacé par une mémoire LIFO, une pile, dans laquelle plusieurs masques sont empilés. Le tout forme une pile, similaire à la pile d'appel, sauf qu'elle est utilisée pour empiler des masques. Un masque est calculé et empilé à chaque entrée dans une structure de contrôle, puis dépilé une fois la structure de contrôle exécutée. L'empilement et le dépilement des masques est effectué par des instructions PUSH et POP, présentes dans le jeu d'instruction du processeur SIMD. Le calcul des masques doit répondre à plusieurs impératifs. * Premièrement, chaque masque se calcule en faisant un ET entre le masque précédent et le masque calculé par l'instruction de test. Cela permet de ne pas réveiller d’élément au beau milieu d'une structure imbriquée. Si in IF désactive certains éléments du vecteur, une condition imbriquée dans ce IF ne doit pas réveiller cet élément. Le fait de faire un ET entre les masques garantit cela. * Deuxièmement, les masques doivent être empilés et dépilés correctement. Au moment de rentrer dans une structure de contrôle, on effectue une instruction de test associée à la structure de contrôle, qui calcule un masque, et on empile le masque calculé. Au moment de sortir de la structure de contrôle, on dépile le masque en question. L'implémentation demande d'utiliser une mémoire LIFO pour stocker la pile de masques, et quelques circuits annexes. Il faut notamment un circuit relié à l'ALU qui récupère les conditions, les résultats des comparaisons, et qui effectue le ET pour combiner les maques. Pour donner un exemple, prenons le code suivant, qui est volontairement simpliste et ne sert qu'à des fins d'explication : <syntaxhighlight lang="c"> if ( condition 1 ) { if ( condition 2 ) { ... } else { ... } Autres instructions } Instructions après le IF... </syntaxhighlight> Imaginons que l'on traite des vecteurs de 8 éléments. Pour le vecteur considéré, la première condition (a > 0) n'est respectée que par les 4 premiers éléments. L'instruction de condition calcule alors le masque correspondant : 1111 0000. Le masque est alors calculé, puis empilé au somment de la pile. La seconde instruction de test, qui teste la variable b, est maintenant valide pour les 4 bits du milieu du masque. Mais n'allez pas croire que le masque correspondant soit 0011 11100 : il faut tenir compte de la condition précédente, qui a éliminé les 4 derniers éléments. Pour cela, on fait un ET logique entre le masque précédent, et le masque calculé par la condition. Le masque au sommet de la pile est donc lu, combiné avec le masque calculé par l'instruction, ce qui donne le masque final. Le masque final est alors empilé au sommet de la pile. On exécute alors l'instruction du IF, en tenant compte du masque qui est au sommet de la pile. Si le IF était plus compliqué, toutes les instructions suivantes tiendraient compte du masque. En fait, le masque est pris en compte tant qu'il n'est pas dépilé. Une fois que le IF est terminé, le masque est dépilé. On passe alors au ELSE, et rebelotte. Le masque pour le ELSE est calculé en combinant le masque au sommet de la pile avec la condition du ELSE. Le masque au sommet de la pile est celui calculé à l'entrée du premier IF, pas le second qui a été dépilé. Les instructions du ELSE sont alors exécutées en tenant compte de ce masque. Une fois qu'elles sont toutes exécutées, le masque est dépilé. Puis vient l'exécution des instructions après le ELSE. Elles utilisent le masque empilé au sommet de la pile, qui correspond à celui à l'entrée du IF. Puis vient le moment d'exécuter les instructions après le IF : pas de masque, on exécute sur tout le vecteur. ===Les compteurs d'activité=== Une variante de la technique précédente remplace la pile de masques par des '''compteurs d'activité'''. La technique est similaire, si ce n'est qu'elle utilise moins de circuits. Avant , on avait une pile de masques de même taille, dont les bits sont à 0 ou 1 suivant que la condition est remplie. La pile de masque ressemble donc à ceci : {|class="wikitable" |- ! masque 1 | 1 || 1 || 1 || 1 |- ! masque 2 | 0 || 1 || 1 || 1 |- ! masque 3 | 0 || 1 || 1 || 1 |- ! masque 4 | 0 || 0 || 0 || 1 |- ! masque 1 | colspan="4" | vide |} Une manière équivalent de représenter cette pile de masque est de compter combien de bits sont à 0 dans chaque colonne. Attention : j'ai bien dit à 0 ! On obtient alors : {|class="wikitable" |- ! masque 1 | 3 || 1 || 1 || 0 |} Et c'est le principe caché derrière la techniques des compteurs d'activité. Chaque élément dans un vecteur, chaque place, se voit attribuer un compteur. Un compteur non-nul indique qu'il ne faut pas prendre en compte l’élément. Ce n'est qu'une fois que le compteur est nul que l'on effectue des opérations sur l’élément associé du vecteur. À chaque fois qu'on entre dans une structure de contrôle, on teste une condition sur chaque élément. Si la condition est respectée pour un élément, alors le compteur ne change pas. Mais si la condition n'est pas respectée, alors on incrémente le compteur associé. En sortant de la structure de contrôle, on décrémente le compteur associé. Notons que les compteurs qui n'ont pas été incrémenté en entrant dans la structure de contrôle ne sont pas décrémenté en sortant. En clair, là où on empilait/dépilait un masque, on se contente d'incrémenter/décrémenter un compteur. Utiliser un compteur en lieu et place d'une colonne entière dans la pile de masque utilise moins de bits. Et c'est sans doute pour cette raison que certaines cartes graphiques, comme les cartes graphiques intégrées d'Intel depuis 2004, utilisent cette technique. ==Les processeurs vectoriels== Les '''processeurs vectoriels''' sont les ancêtres des processeurs à instruction SIMD. Bien qu'ils soient arrivés en premier, ils incorporent diverses techniques que de simples instructions SIMD n'ont pas forcément. C'est paradoxal, mais c'est ainsi. La première différence se manifeste au niveau du jeu d'instruction. Les processeurs vectoriels sont capables de traiter des vecteurs de taille variable, alors que les instructions SIMD usuelles ne gèrent que des vecteurs de taille fixe. Par taille variable, on veut dire que le processeur gère nativement des vecteurs d'une taille allant de 1 à la taille maximale d'un vecteur. La taille maximale d'un vecteur est celle permise par la taille des registres vectoriels, il s'agit de la taille d'un vecteur SIMD équivalent sur les autres processeurs SIMD. La seconde différence est une différence en termes de micro-architecture. Un processeur SIMD actuel dispose d'une unité de calcul très élaborée, capable de faire plusieurs calculs en parallèle, au moins un pour chaque élément du vecteur. Par exemple, si un vecteur contient 16 entiers, l'ALU SIMD doit contenir au moins 16 additionneurs entiers. Mais sur un processeur vectoriel, ce n'est pas le cas : l'unité de calcul est en réalité une unité de calcul entière/flottante normale, mais qui a la particularité d'être pipelinée. Les calculs sont donc effectués en parallèle, mais d'une manière totalement différente. La plupart de ces différences s'expliquent par le fait que les anciens processeurs vectoriels devaient faire avec des limitations en termes de circuits. Ils avaient peu de transistors à leur disposition, ce qui fait que leur unité de calcul devait être la plus simple possible. D'où l'utilisation d'une unité de calcul simple, mais utilisée de manière pipelinée. De plus, les processeurs vectoriels ne possèdent aucune mémoire cache pour les données et se contentent juste de caches d'instruction. De tels caches sont généralement peu utiles quand on manipule des tableaux, chose quasiment systématique quand on travaille avec du parallélisme de données. ===Une unité de calcul pipelinée=== La différence entre processeur vectoriel et SIMD tient dans la façon dont sont traités les vecteurs : les instructions SIMD traitent chaque élément en parallèle, alors que les processeurs vectoriels pipelinent ces calculs ! Par pipeliner, on veut dire que l’exécution de chaque instruction est découpée en plusieurs étapes indépendantes. Au lieu d'attendre la fin de l’exécution d'une opération avant de passer à la suivante, on peut commencer le traitement d'une nouvelle donnée sans avoir à attendre que l'ancienne soit terminée. [[File:Pipeline.png|centre|vignette|upright=2|Pipeline vectoriel.]] Pour donner un exemple, on peut donner l'exemple d'une multiplication flottante effectuée entre deux registres. Son exécution peut être décomposée en plusieurs étapes. Par exemple, on peut avoir 3 étapes : * une première étape E qui va additionner les exposants et gérer les diverses exceptions ; * une étape M qui va multiplier les mantisses ; * et enfin une étape A qui va arrondir le résultat. L’exécution de notre opération flottante sur un vecteur donnerait donc quelque chose dans le genre, où chaque ligne correspond au traitement d'un nouvel élément dans un vecteur, dans un paquet SIMD. [[File:Multiplication vectorielle - pipeline.png|centre|vignette|upright=2|Multiplication vectorielle - pipeline]] : Certains processeurs vectoriels utilisent plusieurs ALU pipelinées pour accélérer les calculs. On peut les voir comme des intermédiaires entre processeur vectoriels et SIMD SWAR. Avec une unité de calcul pipelinée découpée en N étages, on peut gérer N données simultanées : autant qu'il y a d'étapes différentes. Mais ce nombre maximal de données met un certain temps avant d'être atteint. L'unité de calcul met du temps avant d'arriver à son régime de croisière. Durant ce temps, elle n'a pas commencé à traiter suffisamment d’éléments pour que toutes les étapes soient occupées. Ce temps de démarrage est strictement égal du nombre d'étapes nécessaires pour effectuer une instruction. La même chose arrive vers la fin du vecteur, quand il ne reste plus suffisamment d’éléments à traiter pour remplir toutes les étapes. [[File:Startup and dead time vector pipeline.png|centre|vignette|upright=2|Startup and dead time vector pipeline]] Pour amortir ces temps de démarrage et de fin, certains processeurs démarrent une nouvelle instruction sans attendre la fin de la précédente : deux instructions peuvent se chevaucher pour remplir les vides. Par contre, il peut y avoir des problèmes lorsque deux instructions qui se suivent manipulent le même vecteur. Il faut que la première instruction ait fini de manipuler un élément avant que la suivante ne veuille commencer à le modifier. Pour cela, il faut avoir des vecteurs suffisamment qui contiennent plus d’éléments qu'il n'y a d'étapes pour effectuer notre instruction. ===La technique du ''chaining''=== La technique du pipeline peut encore être améliorée dans certains cas particuliers. L'idée est simplement d'ajouter la technique du contournement (''bypass'') à l'unité de calcul. Pour rappel, le contournement connecte la sortie de l'ALU à une de ses entrées, ce qui permet au résultat d'un calcul d'être réutilisable au cycle d’horloge suivant. L'usage du contournement sur les processeurs vectoriels porte le nom de '''''Vector Chaining'''''. Un processeur implémentant le ''chaining''/''bypass'' a toutes ses unités de calcul reliées entre elles : la sortie d'une unité est reliée aux entrées de toutes les autres. Pour comprendre à quoi peut servir cette technique, on peut citer deux grands exemples principaux. Le premier avantage est que l'implémentation des instructions SIMD horizontales est beaucoup plus simple. C'est pour cela que les processeurs vectoriels intègrent souvent des instructions horizontales, et notamment des instructions arithmétiques horizontales. Le contraste avec les processeurs SIMD récents, qui utilisent plusieurs ALU travaillant en parallèle, est frappant. Ces derniers n’intègrent pas d'instructions horizontales arithmétiques, les seules opérations horizontales supportées sont généralement des permutations ou des instructions ''compress/expand'', guère plus. Maintenant, introduisons une autre possibilité par un exemple. Dans cet exemple, on suppose que le processeur dispose d'une ALU pour l'addition et d'une autre pour la multiplication. Imaginons que l'on ait trois vecteurs nommés A, B et C. Pour chaque énième élément de ces paquets, je souhaite effectuer le calcul <math>A_n + B_n \times C_n</math>. En théorie, il faudrait faire d'abord la multiplication, stocker le résultat temporaire de la multiplication dans un registre vectoriel, puis faire l'addition. Mais en rusant un peu, on peut utiliser le pipeline plus efficacement. Une fois que le premier élément de la multiplication du premier vecteur est connu, pourquoi ne pas démarrer l'addition immédiatement après, et continuer la multiplication en parallèle ? Après tout, les deux calculs ont lieu dans des ALUs séparés. Et le contournement permet d'alimenter l'ALU d'addition avec la sortie du circuit multiplieur. [[File:Vector chaining.png|centre|vignette|upright=2|Vector chaining]] ===La gestion des vecteurs de taille variable=== L'usage d'une unité de calcul pipelinée fait que la taille des vecteurs n'est pas forcément fixe. Autant les processeurs SIMD non-vectoriels disposent d'un nombre fixe d'unités de calcul, et peuvent donc gérer des vecteurs de taille fixe, autant ce n'est pas le cas des processeurs vectoriels. Une unité de calcul pipelinée à 16 étage peut utiliser les trois premiers pour traiter un vecteur de 3 élements, les 5 suivants pour un second vecteur de 5, et le reste pour un troisième vecteur. Reste que la gestion des vecteurs de taille variable doit être gérée au niveau du séquenceur, mais surtout : doit être permise au niveau du jeu d'instruction. Pour cela, il faut ajouter au jeu d'instruction de quoi indiquer la taille du vecteur en cours de traitement. Et c'est le rôle d'un registre spécialisé, le '''Vector Length Register''', que de gérer les vecteurs de taille variable. Le ''Vector Length Register'' est un registre qui indique combien d’éléments on doit traiter dans un vecteur. On peut ainsi dire au processeur : je veux que tu ne traites que les 40 premiers éléments présents d'un paquet. Ils sont utilisés pour gérer des tableaux dont la taille n'est pas un multiple d'un vecteur. Quand on arrive à la fin d'un tableau, il suffit de configurer le ''Vector Length Register'' pour ne traiter que ce qu'il faut. [[File:Vector length register.png|centre|vignette|upright=2|Vector length register.]] Il facilite l'usage du '''déroulage de boucle''', une optimisation qui vise à réduire le nombre d'itérations d'une boucle en dupliquant son corps en plusieurs exemplaires. Le compilateur réplique le corps de la boucle (les instructions à répéter) en plusieurs exemplaires dans cette boucle, avant de corriger le nombre d'itérations de la boucle. Par exemple, prenons cette boucle, écrite dans le langage C : <syntaxhighlight lang="c"> int i; for (i = 0; i < 100; ++i) { a[i] = b[i] * 7 ; } </syntaxhighlight> Celle-ci peut être déroulée comme suit : <syntaxhighlight lang="c"> int i; for (i = 0; i < 100; i+=4) { a[i] = b[i] * 7 ; a[i+1] = b[i+1] * 7 ; a[i+2] = b[i+2] * 7 ; a[i+3] = b[i+3] * 7 ; } </syntaxhighlight> Les instructions vectorielles permettent de traiter plusieurs éléments à la fois, ce qui fait que plusieurs tours de boucles peuvent être rassemblés en une seule instruction SIMD. Le déroulage de boucles permet d'exposer des situations où ce regroupement est possible. Dans notre exemple, si jamais notre processeur dispose d'une instruction de multiplication capable de traiter 4 éléments du tableau a ou b en une seule fois, la boucle déroulée peut être vectorisée assez simplement en utilisant une multiplication vectorielle (que nous noterons vec_mul). <syntaxhighlight lang="c"> int i; for (i = 0; i < 100; i+=4) { vec_a[i] = vec_mul ( vec_b[i] , 7 ) ; } </syntaxhighlight> Le déroulage de boucles n'est toutefois pas une optimisation valable pour toutes les boucles. Reprenons l'exemple de la boucle vue plus haut. Si jamais le tableau à manipuler a un nombre d’éléments qui n'est pas multiple de 4, la boucle ne pourra être vectorisée, vu que la multiplication vectorielle ne peut traiter que 4 éléments à la fois. Pour ce faire, les compilateurs utilisent généralement deux boucles : une qui traite les éléments du tableau avec des instructions SIMD, et une autre qui traite les éléments restants avec des instructions non vectorielles. Cette transformation s'appelle le '''strip-mining'''. Par exemple, si je veux parcourir un tableau de taille fixe contenant 102 éléments, je devrais avoir une boucle comme celle-ci : <syntaxhighlight lang="c"> int i; for (i = 0; i < 100; i+=4) { a[i] = b[i] * 7 ; a[i+1] = b[i+1] * 7 ; a[i+2] = b[i+2] * 7 ; a[i+3] = b[i+3] * 7 ; } for (i = 100; i < 102; ++i) { a[i] = b[i] * 7 ; } </syntaxhighlight> Les processeurs vectoriels utilisent le ''Vector Length Register'' pour éviter d'utiliser le strip-mining. Avec ce registre, il est possible de demander aux instructions vectorielles de ne traiter que les n premiers éléments d'un vecteur : il suffit de placer la valeur n dans le ''Vector Length Register''. Évidemment, n doit être inférieur au nombre d’éléments maximal du vecteur. Avec ce registre, on n'a pas besoin d'une seconde boucle pour traiter les éléments restants, et une simple instruction vectorielle peut suffire. L'avantage principal est que cela réduit la portion de code non-parallélisable, donc augmente l'efficience SIMD. Mais l'intérêt n'est pas qu'une question de code. Il faut aussi prendre en compte que cela permet d'utiliser l'ALU plus efficacement. Dès qu'un vecteur court est terminé, le processeur peut immédiatement démarrer le calcul d'un autre vecteur, le vecteur suivant, l'instruction suivante, sans laisser de cycles inutilisés. Avec un processeur SIMD non-vectoriel, on aurait du utiliser de la prédication, donc utiliser seulement une partie des ALU SIMD, ce qui aurait été un gâchis. ===Les accès mémoire=== Les accès mémoire des processeurs vectoriels sont sensiblement les mêmes que ceux des autres processeurs SIMD. Les instructions d'accès mémoire sont les mêmes, on retrouve les modes d'adressages précédents, dont les accès en ''scatter-gather'' et en ''stride''. Sur certains processeurs vectoriels, les instructions vectorielles lisent et écrivent en mémoire RAM, sans passer par des registres : on parle de '''processeurs vectoriels mémoire-mémoire'''. Elles n'avaient pas d'instructions LOAD et STORE, les instructions arithmétiques géraient directement la lecture des opérandes en mémoire, et incrémentaient automatiquement les adresses à lire/écrire. Elles pouvaient gérer des vecteurs de taille arbitraire, la gestion d'un tableau se faisait parfois en une seule instruction ! Le problème est que l'absence de registres pour stocker les données lues réduisait grandement les performances. [[File:Processeur vectoriel mémoire-mémoire.png|centre|vignette|upright=2|Processeur vectoriel mémoire-mémoire]] Les processeurs vectoriels mémoire-mémoire étaient cependant minoritaires, la grosse majorité des processeurs vectoriels récents possèdent des registres vectoriels dédiés aux vecteurs. La plupart sont des architectures de type LOAD-STORE, mais pas toute. De fait, il n'y a pas de lien strict entre adressage des opérandes et processeurs vectoriel (ou SIMT). Seuls les processeurs SIMD de type SWAR ont une restriction là-dessus et sont systématiquement des architectures LOAD-STORE. [[File:Processeur vectoriel à registres vectoriels.png|centre|vignette|upright=2|Processeur vectoriel à registres vectoriels.]] Il faut noter que l'implémentation des accès mémoire est potentiellement différente de celle des autres processeurs SIMD. En principe, on peut utiliser une implémentation identique entre les deux. Mais le fait que les processeurs vectoriels sont pipelinés fait qu'il est préférable d'utiliser une implémentation différente, qui se base sur une RAM ou des caches pipelinés. Vu que l'ALU reçoit une/deux opérandes par cycle, l'idéal est que la mémoire ou le cache aillent au même rythme et soient donc pipelinés.Bien sur, il est possible d'utiliser une implémentation plus classique, avec des caches multiports, un chargement en plusieurs fois des vecteurs depuis le cache, des circuits pour réorganiser les accès mémoire, etc. En théorie, les architectures vectorielles font peu usage de mémoires cache. Les transferts entre registres vectoriels et mémoire RAM sont directs, sans intermédiaire. La raison est que les applications qui tirent parti du SIMD ont une bonne localité spatiale, mais une mauvaise localité temporelle. Or, les caches de données profitent surtout de la localité temporelle pour donner de bonnes performances. De plus, rappelons que les processeurs vectoriels sont assez anciens et datent d'une époque où les transistors étaient précieux. Il valait mieux utiliser des transistors dans un gros cache d'instruction que pour un cache de donnée peu efficace. ==Les systèmes SIMD à plusieurs processeurs : le SIMT== Enfin, terminons avec le dernier type de SIMD, qui date du tout début de l'informatique : celui où on combine plusieurs processeurs non-SIMD. L'idée est de synchroniser plusieurs processeurs de manière à ce qu'ils exécutent la même instruction en même temps, chacun sur des données différentes. On parle d''''exécution ''lockstep''''' des instructions. Toute la difficulté est de garantir la synchronisation des processeurs, garantir que tous les processeurs exécutent la même instruction. Ce qui pose problème quand des branchements sont impliqués, des processeurs pouvant prendre le branchement et pas les autres. ===Les ''array processors'' des temps anciens=== Les premières architectures matérielles conçues pour le parallélisme de données étaient de ce type. On peut par exemple citer l'exemple des Thinking machines CM-1 et CM-2, qui exécutaient tous la même instruction au même cycle d'horloge sur 64000 processeurs minimalistes. Un autre exemple est celui de l'ILLIAC IV. Historiquement, le terme utilisé pour désigner de telles architectures était '''array processors''', encore que ce terme fût réservé aux systèmes multiprocesseurs. Par la suite, le terme ''Single Instruction Multiple Threads'', ou SIMT, a été utilisé. Mais ce terme a ensuite été repris par le marketing de NVIDIA pour désigner une technique précise de SIMD avec prédication, utilisée sur ses cartes graphiques récentes. Le mauvais usage de la terminologie fait que le terme SIMT est devenu polysémique et quelque peu trompeur. L'ILLIAC 4 était un ordinateur un peu particulier, presque unique en son genre. Son architecture était composé d'un processeur principal couplé à plusieurs processeurs secondaires. Il y avait en tout un processeur principal et 64 processeurs secondaires. Le processeur principal chargeait les instructions depuis la mémoire, les décodaient, puis envoyait les instructions aux processeurs secondaires. Le processeur principal était le seul à avoir un ''program counter'', il était utilisé pour gérer les boucles, ainsi que pour configurer les instructions à prédication. Cependant, le processeur principal n'était pas un simple séquenceur : il pouvait exécuter des instructions, avait un accumulateur et des registres, etc. Il prenait en charge toute instruction non-SIMD, alors que les instructions SIMD étaient envoyées aux processeurs secondaires. Les 64 processeurs secondaires avaient chacun : une unité de calcul, un ''local store'' de 2048 flottants de 64 bits, une unité mémoire. Ils n'ont pas de séquenceur ni de ''program counter'', ni de quoi charger une instruction. L'unité de calcul est appelée un ''processing element'' ou PE. Elle intègre un additionneur flottant, de quoi faire des décalages, et une unité de calcul logique. De plus, un PE intègre des registres accumulateurs et de quoi calculer des adresses : un registre d'indice, un registre d'adresse, un circuit de calcul d'adresse. L'unité mémoire connectait l'ALU entière PE et le ''local store'', et elle permettait aussi d'adresser des entrées-sorties. Pour gérer la prédication, les processeurs secondaires pouvaient être activés ou désactivés suivant le résultat des branchements/conditions. Intuitivement, on peut faire le rapprochement avec un processeur SIMD. Le processeur principal regrouperait la partie non-SIMD d'un processeur SIMD, alors que les processeurs secondaires seraient l'unité de calcul SIMD. Cependant, il y a une différence très importante : il n'y a pas de banc de registre vectoriel. À la place, chaque processeur secondaire a son propre banc de registre, et ce sont des registres scalaires ! Une autre différence est que la mémoire RAM est découpée en plusieurs ''local store'' de 2048 adresses, chacun étant relié à une unité de calcul PE. N'oublions pas le système de calcul d'adresse et l'unité LOAD/STORE qui est fusionnée avec l'unité SIMD, chaque PE ayant ses circuits de calcul d'adresse, sa propre unité mémoire, etc. ===Les anciennes cartes graphiques hybrides SIMD-VLIW=== Une technique de SIMD multi-processeur a été utilisée autrefois sur certaines cartes graphiques AMD à architecture Terascale. La différence est que l'architecture en question était multicœur et non multi-processeurs. Elles contenaient plusieurs cœurs VLIW, qui fonctionnaient en ''lockstep''. Un groupe de 16 processeurs VLIWs exécutaient la même opération VLIW. Du moins, c'est ce que semblent dire les [https://www.techpowerup.com/gpu-specs/docs/amd-gcn1-architecture.pdf white papers de présentation de l'architecture GCN]. L'usage de cet hybride SIMD-VLIW était très adapté au traitement graphique. Les cartes graphiques post-années 2000 sont capables d'exécuter des programmes appelés des '''''shaders''''', qui sont utilisés en rendu 3D, pour calculer certaines scènes 3D, et notamment pour calculer les ombres (d'où leur nom de shader, pour shade - ombre). Or, ces shaders sont justement exécutés par la carte graphique, sur les processeurs de shaders. Un processeur de ''shader'' applique un programme/''shader'' sur chaque ''pixel'' ou triangle d'une scène 3D (en fait des fragments et des vertices, mais faisons cette simplification). Chaque triangle ou pixel d'une scène 3D peut être traité indépendamment des autres, ce qui rend le traitement 3D fortement parallèle. Aussi, l'usage du SIMD est assez naturel : chaque processeur exécute un ''shader'' sur un pixel/triangle, utiliser une architecture SIMD parait naturel. Reste à justifier l'usage de plusieurs CPU VLIW. La raison tient dans la manière dont est traité un pixel par un shader. Un pixel est codé en utilisant quatre informations. Premièrement, une couleur codée avec trois nombres flottants, qui représentent une couleur au format RGB avec un mélange de rouge, vert et bleu, chaque couleur rouge/bleu/vert étant codée avec un flottant de 32 bits. La quatrième information est un nombre qui code la transparence du pixel. Généralement, le shader traite différemment la transparence de la couleur RGB, dans le sens où les instructions utilisées ne sont pas les mêmes. Au lieu de traiter couleur RGB et transparence séparément, elles sont traitées en même temps en parallèle, ce qui demande de pouvoir émettre deux instructions : une pour le calcul de la transparence, une autre pour la couleur RGB. Fait amusant, l'instruction utilisée pour gérer la composante RGB est une instruction vectorielle qui travaille sur des vecteurs de 3 couleurs. Les architectures VLIW sont parfaites pour cela. Les cartes graphiques de l'époque de DirectX 9 utilisaient cette méthode qui mélangeait SIMD et VLIW. Elles disposaient de plusieurs cœurs VLIW qui exécutaient le même shader en exécution ''lockstep''. Chaque cœur traitait au moins un pixel à la fois, ou un triangle à la fois. Cependant, nous devons préciser que les cœurs VLIW partagent la même unité de contrôle, ce sont en réalité des chemins de données plus qu'autre chose. L'architecture était aussi combinée avec du ''Fine Grained Multithreading'' afin de pouvoir gérer les accès mémoire, de telles architectures ne pouvant pas se permettre d'utiliser l'exécution dans le désordre. De telles architectures multicœurs à exécution ''lockstep'' permettent quelques optimisations, comme le fait de partager le cache d'instruction entre les cœurs. ==Résumé des différents types de processeurs SIMD== Dans ce chapitre, nous avons appris qu'il existe plusieurs catégories de processeurs SIMD. Les différences entre ces catégories tiennent à la fois dans le jeu d'instruction, mais aussi dans la microarchitecture des processeurs en question. Au niveau du jeu d'instruction, il y a trois paliers, qui sont résumés dans le tableau ci-dessous. {|class="wikitable" |- ! ! Support de la prédication ! Taille des vecteurs ! Instructions horizontales ! Architecture LOAD-STORE ! Pointeur de pile |- ! Processeurs SIMD purs | Non | rowspan="3" | Fixe | rowspan="3" | Limitées à des échanges de données à l'intérieur d'un vecteur, pas d'instructions de réduction | Oui | rowspan="2" | Unique |- ! Processeurs SIMD avec prédication | Oui, par définition | rowspan="3" | Dépend du processeur |- ! Processeurs SIMT | rowspan="2" | Oui, sauf exceptions | Un pointeur de pile possible par élément du vecteur |- ! Processeurs vectoriels | Variable | Toutes supportées, y compris les instructions arithmétiques horizontales | Unique |} Au niveau de la microarchitecture, il y a trois paliers, qui sont résumés dans le tableau ci-dessous. {|class="wikitable" |- ! ! Unité de calcul ! Banc de registre ! Adressage des opérandes |- ! Processeurs SIMD | rowspan="2" | Plusieurs ALU simples non-pipelinée, plusieurs additionneurs/multiplieurs/autres | Banc de registres vectoriels | Architecture LOAD-STORE |- ! Processeurs SIMT | Plusieurs bancs de registres scalaires (non-vectoriels, registres normaux) | rowspan="2" | Dépend du processeur |- ! Processeurs vectoriels | ALU simple unique, pipelinée | Banc de registres vectoriels |} Les processeurs SIMD purs ont des performances différentes des processeurs vectoriels, vu que les premiers font leurs calculs en parallèle, alors que les CPU vectoriels utilisent un pipeline. Mais dans les grandes lignes, les performances sont similaires. Les différences sont grandement atténuées par l'usage du ''vector chaining'', de la gestion des vecteurs de taille variables, et des autres optimisations spécifiques aux processeurs vectoriels. L'avantage est cependant du côté des processeurs SIMD pur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Architectures multithreadées et Hyperthreading | prevText=Architectures multithreadées et Hyperthreading | next=La cohérence des caches | nextText=La cohérence des caches }} {{autocat}} </noinclude> 4oaq2mguj0njoil95u8j1hpbbx718hj 773896 773895 2026-10-01T19:33:01Z Mewtow 31375 /* Les instructions SIMD verticales : plusieurs ALU travaillant en parallèle */ 773896 wikitext text/x-wiki Nous allons maintenant aborder le parallélisme de données, qui consiste à traiter des données différentes en parallèle. De nombreuses situations s'y prêtent relativement bien : traitement d'image, manipulation de sons, vidéo, rendu 3d, etc. Mais pour exploiter ce parallélisme, il a fallu concevoir des processeurs adaptés. Les architectures les plus simples exécutent une instruction sur plusieurs données en parallèle, à l'intérieur d'un processeur. Ces architectures exploitent le parallélisme de données au niveau de l'unité de calcul, celle-ci pouvant exécuter un même calcul sur des données différentes en parallèle. Si on omet quelques exceptions, on peut classer ces architectures dans plusieurs catégories principales : les processeurs à instructions SIMD, les processeurs à instructions SIMD à prédicat, les processeurs SIMT, les processeurs vectoriels. Si les processeurs vectoriels sont assez rares de nos jours, tous les processeurs récents incorporent des instructions SIMD. Quant au SIMT, il est utilisé sur les cartes graphiques modernes. ==Les instructions SIMD : les points communs entre tous les processeurs SIMD== Les processeurs modernes fournissent presque tous des '''instructions SIMD''', qui sont capables de traiter plusieurs éléments en parallèle. Elles travaillent sur des nombres entiers ou flottants regroupés dans ce qu'on appelle un ''vecteur'', qui sont eux-mêmes stockés dans des registres spécialisés. En général, tous les vecteurs ont une taille fixe, peu importe leur contenu. Cela implique que suivant la taille des données à manipuler, on pourra en placer plus ou moins dans un vecteur. Par exemple, un vecteur de 128 bits pourra contenir 4 entiers de 32 bits, 4 flottants 32 bits, ou 8 entiers de 16 bits. [[File:Vector register.png|centre|vignette|upright=2|Contenu d'un vecteur en fonction du type de données utilisé.]] Les vecteurs sont stockés dans des '''registres vectoriels''', aussi appelés '''registres SIMD'''. Un registre vectoriel peut contenir un vecteur complet, pas plus. En conséquence, ils ont une taille assez importante : ils font généralement 128,,256, voire 512 bits, comparé aux 32/64 bits des registres scalaires. Le résultat est que les processeurs SIMD modernes ont des registres SIMD séparés des registres entiers/flottants normaux. Un défaut de cette organisation est que les registres vectoriels sont des registres en plus, qui doivent être sauvegardés lors des commutations de contexte, lors des interruptions, lors des appels systèmes, etc. {| |+ Comparaison entre un processeur sans registres vectoriels, et avec registres vectoriels. |[[File:Non-SIMD cpu diagram1.svg|vignette|upright=1.5|CPU Non-SIMD]] |[[File:SIMD cpu diagram1.svg|vignette|upright=1.5|CPU SIMD]] |} Les instructions SIMD peuvent être rassemblées en deux grands groupes : les horizontales et les verticales. Les '''instructions SIMD verticales''' travaillent en parallèle sur les éléments qui sont "à la même place" dans deux vecteurs. Elles peuvent additionner ou multiplier deux vecteurs, par exemple. Pour prendre l'exemple d'une instruction d'addition vectorielle, celle-ci va additionner ensemble les données qui sont à la même place dans deux vecteurs, et placer le résultat dans un autre vecteur, à la même place. Les '''instructions SIMD horizontales''' partent d'un vecteur et ont pour résultat un simple nombre. Elles peuvent calculer la somme ou le produit des éléments d'un vecteur, renvoyer le nombre d’éléments nuls dans un vecteur, etc. [[File:Instructions SIMD.png|centre|vignette|upright=2.5|Instructions SIMD]] Les instructions SIMD sont difficilement utilisables dans des langages de haut niveau et c'est donc au compilateur de traduire un programme avec des instructions vectorielles. Les transformations qui permettent de traduire des morceaux de programmes en instructions vectorielles (déroulage de boucles, strip-mining) portent le nom de vectorisation. ===Les instructions SIMD arithmétiques verticales=== Les instructions SIMD sont essentiellement des instructions arithmétiques, comme des additions, des soustractions, des multiplications, éventuellement des opérations bit à bit, et quelques autres. Suivant la taille des données, et le type de celle-si, on devra effecteur des instructions différentes. Par exemple, on devra utiliser deux instructions d'addition différentes suivant qu'on manipule des flottants 64 bits ou des entiers 32 bits. De même, l'instruction pour additionner des vecteurs d'entiers de 16 bits sera différente de celle manipulant des vecteurs d'entiers de 32 bits. L'addition et la multiplication peuvent générer des débordements d'entier. Pour les instructions non-SIMD, le débordement d'une addition est géré par un bit de retenue, stocké dans le registre d'état. Mais pour les instructions SIMD, ce n'est pas une solution très facile à implémenter. À la place, les additions et soustractions utilisent généralement l'arithmétique saturée, à savoir que lorsqu'une addition déborde, le résultat est la valeur maximale représentable dans un entier. Pour la multiplication, les instructions non-SIMD génère un résultat qui est codé sur le double du nombre de bits. Une multiplication de deux nombres 64 bits donnera un résultat sur 128 bits, par exemple. Pour gérer cela, les multiplications non-SIMD stockent ce résultat dans deux registres, ou ne conservent que les 64 bits de poids faible. D'autres solutions sont possibles, mais ces deux là sont les plus utilisées. Pour les instructions SMID, une autre solution est préférée car plus simple pour de telles instructions : fournir deux instructions : une qui calcule les 64 bits de poids faible du résultat, une autre pour les 64 bits de poids fort. Les processeurs SIMD étant utilisés pour du traitement d'image ou du rendu 2D/3D, ils supportent généralement des opérations mathématiques assez complexes sur des nombres flottants. Il n'est pas rare d'avoir des instructions SIMD pour le calcul de l'inverse d'un nombre, de sa racine carrée, l'inverse d'une racine carrée, des fonctions trigonométriques, etc. L'implémentation matérielle de telles instructions est généralement très complexe et gourmande en circuits, ce qui fait que les concepteurs de processeurs rusent. Sauf sur certains processeurs assez peu fréquents, les instructions mathématiques complexes ne calculent pas un résultat exact, mais une approximation du résultat. Utiliser un résultat approximatif ne pose pas de problème pour de telles applications, le rendu d'image ou 3D s'en accommodant parfaitement, contrairement au calcul scientifique. ===Les instructions SIMD de manipulation de données intra-vecteur=== Après avoir vu les instructions SIMD verticales, il est temps de voir les instructions SIMD horizontales. Pour rappel, celles-ci travaillent un vecteur unique, dont elles réarrangent ou modifient les éléments. Tout cela sera plus parlant une fois qu'on aura donné quelques exemples. Mais avant toute chose, nous allons séparer les instructions horizontales en deux sous-types, fondamentalement différents. Le premier sous-type change de place les données dans un vecteur. Le second type est celui des instructions horizontales arithmétiques/logiques habituelles, qu'on verra plus tard. La distinction entre les deux est très importante, comme on le verra plus tard. [[File:SIMD instruction data movement - One operand.png|vignette|upright=1.5|Instructions SIMD de mouvement de données à une opérande.]] Les instructions SIMD horizontales les plus communes bougent des données à l'intérieur des vecteurs, ce qui leur vaut le nom d''''instructions de manipulation de vecteur'''. Les plus simples sont celles qui ont une seule opérande, à savoir qui prennent un vecteur comme opérande et renvoient un vecteur résultat. Les plus intuitives sont les instructions de '''permutation''', dont le nom est assez explicite. Elles changent de place des données dans un vecteur. Dans le cas le plus compliqué, elles prennent deux vecteur : le vecteur dans lequel faire la permutation, et un vecteur qui indique comment faire la permutation. Le vecteur précise, pour chaque élement du vecteur, où il doit aller après la permutation. L'instruction effectue la permutation des entiers/flottants/octets dans le vecteur. Les instructions '''''compress''''' et '''''expand''''' sont elles aussi spécifiques aux vecteurs. L'instruction ''compress'' prend un vecteur en opérande, sélectionne certaines valeurs présentes dedans, et les stocke dans un vecteur de sortie. Elle regroupe les valeurs sélectionnées dans le début du vecteur, et laisse les cases inoccupées à 0. Par exemple, pour un vecteur de 16 entiers, elle permet de sélectionner 5 entiers dedans, et les place dans un vecteur résultat qui ne contient que ces 5 valeurs au début du vecteur, le reste est à 0. L'instruction ''expand'' fait l'inverse : elle prend un vecteur crée par l'instruction ''compress'' (ou du moin qui a le même format), prend toutes les valeurs non-nulles dedans, et les disperse dans un vecteur de sortie, en les mettant à la place désirée. Les autres instructions SIMD horizontales prennent deux opérandes, voire aucune opérande vectorielle (elles prennent un nombre, pas un vecteur). Les instructions '''''shuffle''''' prennent plusieurs vecteurs, sélectionnent les éléments adéquats dans chaque vecteur, et les regroupe dans un vecteur résultat. Par exemple, prenons 2 vecteurs de 16 entiers chacun. Une instruction ''shuffle'' peut prendre 8 entiers dans chaque vecteur et les regrouper dans un vecteur résultat unique de 16 entiers. Ou encore, elle peut prendre trois vecteurs de 16 flottants chacun, prendre 4 flottants dans le premier, 10 dans le second et 2 dans le troisième, et regrouper le tout dans un vecteur résultat de 16 flottants. Il arrive que les deux cas précédents correspondent à des cas séparés, à deux instructions de type ''shuffle'', mais qui fonctionnent différemment. Le premier type prend les N premiers éléments du premier vecteur, puis prend les élements manquant à partir de la fin du vecteur. Le second type prend un élément sur deux dans chaque vecteur, mais avec un décalage d'un rang entre les deux vecteurs. Il y a aussi les instructions dites de '''''broadcast''''', qui copient une valeur unique dans un vecteur entier. Elles sont surtout utilisées pour initialiser des tableaux avec une valeur unique, ou pour remplir un vecteur avec des données prédéterminées pour certains calculs impliquant des constantes. [[File:SIMD instruction exemple.png|centre|vignette|upright=1.5|Instructions SIMD de mouvement de données qui ne sont pas à une opérande.]] ===Les instructions SIMD de réduction=== Il existe des instructions horizontales de type arithmétiques. La plus simple est celle qui additionne les différents entiers/flottants d'un vecteur. Elle est utilisée pour accélérer une opération précise : faire la somme d'un tableau d'entier/flottants. On peut aussi citer les opérations maximum et minimum, qui renvoient le plus grand/petit élément d'un vecteur. Un point important avec ces opérations est qu'elles prennent un vecteur, mais renvoie un résultat unique, un scalaire, un nombre entier/flottant seul. Elles sont parfois appelées des '''instructions de réduction'''. Les instructions de réduction sont généralement la spécificité des processeurs vectoriels, les processeurs SIMD récents n'en ont pas. La raison est que leur implémentation matérielle est généralement assez compliquée. Contrairement aux instructions verticales, elles ont une forme de parallélisme de donnée très limitée. Elles ne travaillent pas exactement sur des données indépendantes, elles font des calculs dont le résultat est utilisé par d'autres, il y a des dépendances de données entre éléments du vecteur. Cependant, il faut savoir que l'on peut émuler une instruction de réduction en utilisant des instructions verticales couplées à des instructions de manipulation de vecteur. Par exemples, prenons le cas d'une instruction de réduction qui additionne entre eux les éléments d'un vecteur. On peut l'émuler en utilisant une addition verticale entre deux vecteurs, et une instruction ''compress''. L'idée est la suivante : on coupe le vecteur initial en deux avec deux instructions ''compress'', ce qui donne deux sous-vecteurs. Puis, on additionne les deux vecteurs résultat entre eux. Puis on répète les deux étapes précédentes jusqu'à obtenir la somme finale. Le fait que l'on puisse émuler les instructions de réduction est la raison principale qui explique que les processeurs SIMD récents n’intègrent pas d'instructions de réduction. Ils se débrouillent avec des instructions SIMD de manipulation de vecteur, et des instructions SIMD verticales. C'est un bon compromis entre cout en circuits et performance. ===Les accès mémoire=== La gestion des accès mémoire est assez hétéroclite, la façon de faire étant différente selon les architectures. Dans le cas le plus simple, les données d'un vecteur sont contigües en mémoire et les instructions ont juste à préciser l'adresse mémoire du début du vecteur, qui est généralement dans un registre d'adresse spécialisé, ou un registre non-SIMD. Avec des vecteurs de 8 octets, toute instruction d'accès mémoire de ce type va lire ou écrire des blocs de 8 octets. L'adresse de départ de ces blocs est soumise à des contraintes d'alignement sur les jeux d'instructions comme le SSE, le MMX, etc. La raison à cela est que gérer des accès non alignés en mémoire rend les circuits de lecture/écriture en mémoire plus complexes. En contrepartie, ces contraintes compliquent l'utilisation des instructions SIMD par le compilateur. D'autres modes d'adressage des vecteurs permettent à une instruction de charger des données dispersées en mémoire pour les rassembler dans un vecteur. On peut notamment citer l'existence d'accès mémoires en stride et en scatter-gather. L''''accès en stride''' regroupe des données séparées par un intervalle régulier d'adresses. Ce mode d'accès a besoin de l'adresse initiale, de celle du premier élément du vecteur, et de la distance entre deux données en mémoire. Il permet aux instructions de mieux gérer les tableaux de structures, ainsi que les tableaux multi-dimensionnels. Lorsqu'on utilise de tels tableaux, il arrive assez souvent que l'on n'accède qu'à des éléments tous séparés par une même distance. Par exemple, si on fait des calculs de géométrie dans l'espace, on peut très bien ne vouloir traiter que les coordonnées sur l'axe des x, sans accès sur l'axe des y ou des z. Les instructions d'accès mémoire en enjambées gèrent de tels cas efficacement. Les processeurs SIMD incorporent aussi les accès en '''''scatter-gather'''''. De tels accès prennent en opérande un vecteur contenant des adresses mémoires, et lit/écrit chacune de ses adresses mémoire indépendamment. Les accès en ''scatter-Gather'' peuvent être vus comme une généralisation de l'adressage indirect à registre aux vecteurs, chaque élément du vecteur étant adressé via adressage indirect. Les accès en ''gather'' sont des lectures : elles prennent un vecteur d'adresse, lisent chaque adresse, et regroupent toutes les données lues dans un vecteur, dans un registre vectoriel. Les accès en ''scatter'' sont l'équivalent pour les écritures. Ils prennent un vecteur d'opérande pour les données à écrire, un vecteur pour les adresses. [[File:Cuda4.png|centre|vignette|upright=2|Adressage en scatter-gather]] Un autre version des accès en ''scatter-gather'' n'utilise pas des adresses mémoire, amis des indices. Le vecteur opérande ne contient pas des adresses, mais des indices qui sont combinés à une adresse de base pour calculer plusieurs adresses. Un tel mode d'adressage permet de faciliter l'implémentation de certaines structures de données, appelées des vecteurs de Liffe. Ils sont très utilisés pour gérer les matrices creuses, des matrices où une grande partie des éléments sont nuls et où, dans un souci d'optimisation, seuls les éléments non nuls de la matrice sont stockés en mémoire. Lors d'une instruction de ''scatter'' ou de ''gather'', tous les accès mémoire n'ont pas lieu en même temps, certains accès mémoire seront terminés avant les autres. Les accès peuvent se faire dans le désordre et de nombreuses optimisations matérielle en profitent pour gagner en performance. Il existe cependant un cas où les accès mémoire doivent se faire dans un ordre bien précis, généralement du premier élément vers le dernier : lorsqu'une instruction ''scatter'' effectue plusieurs écritures à la même adresse. C'est parfaitement possible, et c'est le seul cas où il est intéressant de forcer un ordre pour les écritures. En théorie, une instruction SIMD est un tout, c'est à dire qu'elle ne doit pas modifier les registres SIMD avant que tous les accès mémoire se soient terminés. Cependant, il est possible qu'une partie des accès mémoire déclenchent des défauts de page ou lèvent des exceptions matérielles, alors que les autres se sont terminés. En théorie, le processeur est censé se débarrasser du résultat des accès mémoire terminés. Mais ce serait un gachis de performance, ce qui fait que le processeur viole la règle voulant que les registres SIMD soient mis à jour en une seule fois à la fin de l'instruction de ''scatter-gather''. Dès qu'un accès mémoire se termine, il écrit son résultat dans le registre SIMD, à l'endroit adéquat. Cependant, ce comportement pose un problème. Lorsqu'une exception survient, comme lors d'un défaut de page, le processeur exécute la routine d'interruption associée, puis redémarre l'instruction fautive, le second essai étant le bon. Ici, le processeur ne réexecute pas l'instruction à l'identique, mais seulement les accès non-terminés. Mais comment le processeur sait-il quelles accès mémoire redémarrer ? Il doit pour cela mémoriser quels sont les accès mémoire terminés. Il utilise pour cela un registre architectural appelé le '''masque de complétion'''. Le registre est forcément architectural car il doit être préservé lors d'un changement de contexte, l’exécution de l'exception matérielle forçant un changement de contexte. ===Exemple avec les processeurs x86=== [[File:PD-20060908-SSE3-01.svg|vignette|Chronologie des extensions x86 SIMD.]] Après avoir vu la théorie sur les instructions SIMD, il est temps de voir un exemple concret : celui des instructions SIMD des processeurs x86, présents dans nos PC. Le jeu d'instruction des PC qui fonctionnent sous Windows est appelé le x86. C'est un jeu d'instructions particulièrement ancien, apparu en 1978. Depuis, des '''extensions x86''' ont ajouté des instructions au x86 de base. On peut citer par exemple les extensions MMX, SSE, SSE2, ''3dnow!'', etc. Les premières instructions SIMD furent fournies par une extension x86 du nom de MMX, introduite par Intel en 1996 sur le processeur Pentium MMX. Le MMX a perduré durant quelques années avant d'être remplacé par les extensions SSE. La même année, AMD sorti d'expansion ''3DNow!'', qui ajoutait 21 instructions SIMD similaires à celles du MMX. Celui-ci fût suivi du ''3DNow!+'' quelques années plus tard. Le SSE fût ensuite décliné en plusieurs versions avant d'être "remplacé" par l'AVX. Une grande quantité de ces extensions x86 sont des ajouts d'instructions SIMD. L’'''extension MMX''' ajoutait pas mal d'instructions SIMD assez basiques, essentiellement des instructions arithmétiques : addition, soustraction, opérations logiques, décalages, rotations, mise à zéro d'un registre, etc. La multiplication est aussi supportée, mais avec quelques petites subtilités, via l'instruction PMULLW. Les instructions MMX ne mettent pas le registre d'état à jour et ne préviennent pas en cas d'overflow ou d'underflow si ceux-ci arrivent (pour les instructions qui ne travaillent pas en arithmétique saturée). Le MMX introduisait 8 registres vectoriels, du nom de MM0, MM1, MM2, MM3, MM4, MM5, MM6 et MM7. Ils ne pouvaient contenir que des nombres entiers et faisaient 64 bits. Ils avaient cependant un léger défaut, qui a nui à l'adoption du MMX. Pour rappel, avant le MMX, les flottants étaient gérés par l'extension x87, qui définit 8 registres flottants de 80 bits. Et chaque registre MMX correspondait aux 64 bits de poids faible d'un registre flottant x87 ! Le système d'''alias'' de registres typique des CPU x86 a encore frappé ! En conséquence, il était impossible d'utiliser en même temps l'unité de calcul flottante et l'unité MMX. Par contre, sauvegarder les registres lors d'un changement de contexte, d'une interruption, ou d'un appel de fonction était très simple : la sauvegarde des registres de la FPU x87 suffisait. [[File:MMX-FPU-Register.JPG|centre|vignette|upright=2|Registres MMX et FPU x87.]] [[File:XMM registers.svg|droite|vignette|XMM registers]] Dans les années 1999, une nouvelle extension SIMD fit son apparition sur les processeurs Intel Pentium 3 : le '''Streaming SIMD Extensions''', abrévié SSE. Ce SSE fut ensuite complété, et différentes versions virent le jour : le SSE2, SSE3, SSE4, etc. Cette extension fit apparaitre 8 nouveaux registres, les registres XMM. Sur les processeurs 64 bits, ces registres sont doublés et on en trouve donc 16. En plus de ces registres, on trouve aussi un registre d'état qui permet de contrôler le comportement des instructions SSE : celui contient des bits qui permettront de dire au processeur que les instructions doivent arrondir leurs calculs d'une certaine façon, etc. Ce registre n'est autre que le registre MXCSR. Chose étrange, seuls les 16 premiers bits de ce registre ont une utilité : les concepteurs du SSE ont surement préférés laisser un peu de marge au cas où. La première version du SSE contenait assez peu d'instructions : seulement 70. Le SSE première version ne fournissait que des instructions pouvant manipuler des paquets contenant 4 nombres flottants de 32 bits (simple précision). Je ne vais pas toutes les lister, mais je peux quand-même dire qu'on trouve des instructions arithmétiques de base, avec pas mal d'opérations en plus : permutations, opérations arithmétiques complexes, autres. Petit détail : la multiplication est gérée plus simplement et l'on a pas besoin de s’embêter à faire mumuse avec plusieurs instructions différentes pour faire une simple multiplication comme avec le MMX. On peut quand même signaler une chose : des instructions permettant de contrôler le cache firent leur apparition. On retrouve ainsi des instructions qui permettent d'écrire ou de lire le contenu d'un registre XMM en mémoire sans le copier dans le cache. Ces instructions permettent ainsi de garder le cache propre en évitant de copier inutilement des données dedans. On peut citer par exemple, les instructions MOVNTQ et MOVNTPS du SSE premiére version. On trouve aussi des instructions permettant de charger le contenu d'une portion de mémoire dans le cache, ce qui permet de contrôler son contenu. De telles instructions de prefetch permettent ainsi de charger à l'avance une donnée dont on aura besoin, permettant de supprimer pas mal de cache miss. Le SSE fournissait notamment les instructions PREFETCH0, PREFETCH1, PREFETCH2 et PREFETCHNTA. Autant vous dire qu'utiliser ces instructions peut donner lieu à de sacrés gains si on s'y prend correctement ! Il faut tout de même noter que le SSE n'est pas seul "jeu d'instruction" incorporant des instructions de contrôle du cache : certains jeux d'instruction POWER PC (je pense à l'Altivec) ont aussi cette particularité. Avec le '''SSE2''', de nouvelles instructions furent ajoutés, permettant d'utiliser des nombres de 64, 16 et 8 bits dans chaque vecteur. Le SSE2 incorporait ainsi pas moins de 144 instructions différentes. Ce qui commençait à faire beaucoup. Puis, vient le '''SSE3''', avec ses 13 instructions supplémentaires. Pas grand-chose à signaler, si ce n'est que des instructions permettant d'additionner ou de soustraire tous les éléments d'un paquet SSE ensemble, des instructions pour les nombres complexes, et plus intéressant : les deux instructions MWAIT et MONITOR qui permettent de paralléliser plus facilement des programmes. Le '''SSE4''' fut un peu plus complexe et fut décliné lui-même en 2 versions. Le SSE4.1 introduit ainsi des opérations de calcul de moyenne, de copie conditionnelle de registre (un registre est copié dans un autre si le résultat d'une opération de comparaison précédente est vrai), de calcul de produits scalaire, de calcul du minimum ou du maximum de deux entiers, des calculs d'arrondis, et quelques autres. Avec le SSE4.2, le vice à été poussé jusqu'à incorporer des instructions de traitement de chaines de caractères. [[File:AVX registers.svg|vignette|Registres AVX.]] Avec l''''AVX (Advanced Vector eXtensions)''', on retrouve 16 registres d'une taille de 256 bits, nommés de YMM0 à YMM15 et dédiés aux instructions AVX. Ils sont partagés avec les registres XMM : les 128 bits de poids faible des registres YMM ne sont autres que les registres XMM. L'AVX complète le SSE et ses extensions, en rajoutant quelques instructions, et surtout en permettant de traiter des données de 256 bits. Son principal atout face au SSE est que les instructions AVX permettent de préciser le registre de destination en plus des registres d'opérandes. Avec le SSE et le MMX, le résultat d'une instruction SIMD était écrit dans un des deux registres d'opérande manipulé par l'instruction. Il fallait sauvegarder son contenu si on en avait besoin plus tard, ce qui n'est plus nécessaire avec l'AVX. ==La performance des processeurs SIMD== Il est intéressant de comparer la performance d'un processeur SIMD et celle d'un processeur normal, sans SIMD. Mais cet exercice est compliqué par le fait que les processeurs non-SIMD sont très nombreux : entre les CPU superscalaires, à émission dans l'ordre, exécution dans le désordre, ceux sans rien de tout cela, on a de quoi être perdu. Dans ce qui suit, nous allons comparer deux processeurs basiques sans pipeline : un avec SIMD, un autre sans. Comparé à un processeur sans pipeline, on s'attend à ce que la performance soit augmentée d'un facteur N, avec N le nombre maximal d'entiers/flottants que l'on peut mettre dans un vecteur. Si une architecture SIMD fait N calculs en parallèles, alors les performances sont censées être multipliées par N. Cela laisse penser que plus les vecteurs sont longs, plus le gain en performance est important. Il s'agit là d'un résultat basique, mais la vraie vie est différente. Un premier problème est que ce résultat ne tient que si la mémoire RAM et les caches suivent. En effet, faire N calculs en parallèle demande de lire/écrire N fois plus de données. Les données sont souvent stockées dans des registres vectoriels, ce qui fait que la pression sur le banc de registre est assez importante. Mais cela signifie aussi que les instructions d'accès mémoire doivent lire/écrire N fois plus de données. Il faut alors ajouter des ports sur le cache, élargir le bus mémoire, augmenter la taille des lignes de cache, etc. Le débit binaire des caches et de la mémoire RAM deviennent rapidement des points limitants, qui réduisent le gain en performance des instructions SIMD. Et ne parlons pas de l'interaction avec la mémoire virtuelle : vu qu'on lit/écrit par paquets de données, cela signifie qu'on traverse une page mémoire plus vite, ce qui fait que les défauts de page sont lus fréquents. Un processeur SIMD a intérêt à être combiné à une RAM et des caches solides, très performants. C'est le cas sur les processeurs modernes, mais cela a pose des problèmes pour les processeurs vectoriels, et a mené à leur abandon progressif. Mais passons outre ce problème et regardons la seconde raison qui font que ce résultat est naïf : la loi d'Amdhal ! Toutes les portions d'un programme ne sont pas accélérées par des instructions SIMD, il y a des portions dont les calculs ont des dépendances de données, d'autres qui ne sont pas parallélisables, etc. Une partie du programme est donc impossible à véctoriser (i.e optimiser pour le SIMD), une l'autre l'est. Pour simplifier les explications, on suppose qu'une instruction SIMD prend le même nombre de cycles que son équivalent non-SIMD. Par exemple, une addition SIMD prend le même temps qu'une addition normale. Dans ce cas, la portion non-vectorisable du code reste inchangée, alors que la portion vectorisable est divisée par N, par la largeur du vecteur. On retombe sur une formulation identique à la loi d'Amdhal sur le fond. Une manière de quantifier le tout est d'utiliser la métrique de l'efficience SIMD. Elle est calculée en divisant deux grandeurs. La première est elle-même un rapport : c'est le gain obtenu en terme de nombre d'instructions exécutées. Prenons un programme qui exécute I instructions avant vectorisation, et qui en exécute I/X après. Le gain s'exprime comme suit : : <math>G = \frac{I}{I/X} = X</math> Maintenant, prenons ce gain est divisons-le par la largeur d'un vecteur. On obtient alors l'efficience SIMD : : <math>\text{Efficience SIMD} = \frac{G}{N} = \frac{\frac{I}{I/X}}{N}</math> Notons que le gain et la largeur SIMD ne sont égales que dans un cas bien précis : tout le code est vectorisable. Il s'agit donc d'une reformulation du gain de la loi d'Amdhal, mais dans laquelle le pourcentage de code série est caché. Il est intéressant de regarder l'efficience SIMD quand on augmente la portion du code série, ainsi que la taille des vecteurs. Prenons un cas assez généreux, réaliste pour certaines applications très adaptées au SIMD : 1% du code est non-vectorisable, 99% profite du SIMD. L'efficience SIMD varie alors assez rapidement avec la taille des vecteurs. De 99% pour N=2, elle descend à 98% pour N=16, et tombe sous les 50% pou N=128. La conséquence est que les très grandes tailles de vecteurs ne sont pas vraiment utiles, le rendement est décroissant avec la taille des vecteurs. ==Les processeurs SIMD à registres généraux== Le premier type de processeur SIMD que nous allons voir est celui des processeurs de type '''SWAR''' (''SIMD Within A Register''). Le terme SWAR est un terme polysémique dont le sens a beaucoup changé au fil du temps. Ici, nous allons l'utiliser dans la définition la plus stricte : celle où on effectue du SIMD dans les registres généraux du processeur, sans registres spécialisés. Il s'agit d'une forme de SIMD qui est aujourd'hui peu utilisée, mais qui a été la première forme de SIMD utilisée dans des processeurs grand public commerciaux. L'idée est d'améliorer un petit peu un processeur normal, non-SIMD. Un processeur usuel contient des registres généraux de 32 à 64 bits, qui stockent chacun un opérande de même taille que le registre. D'anciens processeurs avaient des instructions pour effectuer des calculs simples, sur des opérandes plus courts. Par exemple, les processeurs x86 32 bits sont capables de faire des calculs sur 8, 16, ou 32 bits. Mais on ne pouvait placer qu'un seul opérande de 8, 16 ou 32 bits dans un registre général, ce qui est un léger gâchis. Le SWAR est une amélioration de cette technique qui vise à mieux utiliser les registres et l'ALU. L'idée est de regrouper plusieurs opérandes de 8, 16, voire 32 bits, dans un seul registre général. De plus, on ajoute des instructions SIMD d'addition/soustraction/autres, qui lisent des opérandes 8/16/32 bits dans ces registres généraux. Par exemple, sur un processeur 32 bits, on peut ajouter une opération d'addition qui lit deux registres de 32 bits, pour récupérer quatre opérandes de 16 bits (deux par registre) et effectue deux additions 16 bits en même temps dans l'ALU. Et il y a la même chose pour d'autres opérations, comme la soustraction, la comparaison, etc. Les instructions SIMD disponibles sur ces processeurs se limitent généralement à des additions, soustractions, comparaisons, mais guère plus. De plus, il s'agit d'instructions entières, il n'y a pas d'instructions SIMD flottantes. La raison à cela est très simple, mais nous l'expliquerons plus bas, dans la section sur l'implémentation. Disons simplement que de tells architectures visent une économie en circuit maximale. Elles implémentent les instructions SIMD en utilisant le moins de circuits possibles, ce qui fait qu'elles réutilisent les registres généraux et n'ont pas de registres SIMD séparés. ===Les extensions/jeux d'instructions de type SWAR=== Les premiers processeurs à intégrer du SWAR étaient les processeurs DEC Alpha, avec l'extension multimédia '''''Motion Video Extensions'''''. Les processeurs en question étaient les processeurs Alpha 21164PC (PCA56 and PCA57), Alpha 21264 (EV6) and Alpha 21364 (EV7). Les instructions SIMD disponibles étaient très simples et se résumaient à quelques comparaisons : trouver le maximum ou le minimum de deux opérandes de 8/16 bits, quelques instructions de permutation. Les extensions '''''Multimedia Acceleration eXtensions''''' des processeurs Hewlett-Packard PA-RISC sont aussi de ce type, mais disposent de plus d'instructions SIMD. La première version, appelée MAX-1, était disponible sur les processeurs 32 bits de la marque, à partir du processeur PA-7100LC. Les instructions disponibles étaient des additions et soustractions d'opérandes 16 bit. Il y avait en tout trois instructions d'addition et trois pour la soustraction. En tout, il y avait : * une instruction d'addition/soustraction non-signée utilisant l'arithmétique modulaire ; * une instruction d'addition/soustraction signée en arithmétique modulaire ; * une instruction d'addition/soustraction signée en arithmétique saturée. Outre les additions/soustractions, il y avait aussi une instruction pour calculer la moyenne de deux opérandes 16 bits, ainsi qu'une addition fusionnée avec un décalage. La seconde version, appelée MAX-2, ajouta des instructions d'additions fusionnées avec des décalages, mais aussi des instructions de permutation. De plus, cette version était disponible sur les processeurs 64 bits de la marque, ce qui fait que les registres étaient eux aussi de 64 bits. Ils pouvaient contenir deux fois plus d'opérandes 16 bits. Il n'y avait de gestion d'opérandes 32 bits pour les extensions SIMD. ===L'implémentation matérielle=== L'avantage de cette technique est la grande simplicité d'implémentation. On n'ajoute pas de registres SIMD séparés, ce qui a de nombreux avantages : économie de circuits car pas besoin d'un second banc de registre, pas besoin de sauvegarder des registres SIMD en plus lors d'un appel système/interruption, etc. L'implémentation a juste besoin de modifier le décodeur et l'unité de calcul. Le décodeur est modifié pour ajouter des instructions en plus, rien de spécifique au SWAR. Les modifications de l'unité de calcul sont spécifiques au SWAR et modifient la manière dont elle gère les retenues. Une première solution est d'utiliser une ALU bit-slicée, ce qui est l'idéal pour les unités de calcul entières. Par exemple, pour une ALU 32 bits entière, on peut la découper en 4 unités de calcul 8 bits. Pour une opération SIMD avec 4 opérandes 8 bits, les quatre ALU fonctionnent en parallèle, il n'y a pas transmission des retenues. Pour les calculs SIMD avec des opérandes de 16 bits, on regroupe les ALU par paire et on propage les retenues à l'intérieur d'une paire, mais pas entre les paires. Enfin, pour un calcul non-SIMD, les opérandes de 32 bits, on propage les retenues normalement, d'une ALU 8 bits vers la suivante. Une autre solution prend un additionneur 32/64 bits normal, mais mets à 0 les retenues. L'implémentation est très simple avec un additionneur à anticipation de retenues, où les retenues sont calculées en avance, avant de faire l'addition proprement dite, par un circuit d'anticipation de retenue. Il suffit alors d'ajouter un circuit de masquage en sortie du circuit d'anticipation de retenue, qui met à zéro les retenues adéquates. La gestion des débordements demande d'ajouter des circuits, ou du moins de modifier ceux déjà présents dans l'ALU. Typiquement, les circuits pour gérer l'arithmétique saturée sont un peu modifiés pour effectuer la mise à 1111...111 octet par octet et chaque octet est masqué si besoin. [[File:ALU modifiée pour implémenter du SWAR.png|centre|vignette|upright=3|ALU modifiée pour implémenter du SWAR]] Notons que la technique ne s'applique qu'aux additions, et aux opérations dérivées comme la soustraction et les comparaisons (ces dernières sont des soustractions déguisées). Mais les multiplications ou divisions ne peuvent pas s'implémenter simplement en modifiant des ALU existantes, du moins pas avec des modifications simples. Aussi, les processeurs qui utilisent cette technique se bornent aux additions et opérations dérivées, pas plus. La gestion des opérations de permutation est quant à elle très simple : beaucoup de processeurs disposent déjà d'instructions de permutation d'octets, qui sont l'équivalent d'instructions SIMD de permutation pour les registres généraux. Nous en avions déjà parlé dans le chapitre sur le langage machine et l'assembleur, quand nous avions fait la liste des instructions les plus courantes. De telles instructions de permutation d'octet sont utiles pour gérer le boutisme ou effectuer quelques manipulations assez rares. Pas besoin de rajouter une ALU dédiée, celle-ci est déjà présente de base, du moins si les instructions de permutation d'octet sont déjà présentes. ==Les processeurs SIMD purs (''packed SIMD'')== Maintenant que nous avons vu les instructions SIMD, passons maintenant aux processeurs eux-même. Tous les processeurs que nous allons voir dans ce qui suit supportent des instructions SIMD. Mais leur implémentation matérielle, leur micro-architecture, n'est pas la même. Ils ont aussi quelques différences en termes de jeu d'instruction, mais qui sont fortement liées à l'implémentation matérielle. Nous allons ici voir les '''processeurs SIMD purs''', aussi dits de type ''Packed SIMD''. Ils implémentent des instructions SIMD sans rien de plus, juste le strict minimum : pas de prédication, pas de vecteurs de taille variable, presque pas d’instructions horizontales, architecture de type LOAD-STORE. De tels processeurs utilisent plusieurs ALU entières/flottantes qui travaillent en parallèle. De plus, ils disposent de registres SIMD séparés des registres généraux. Voyons ces points immédiatement. * La première distinction est que les vecteurs sont stockés dans des registres séparés des registres généraux, appelés les '''registres SIMD''', ou ''registres vectoriels''. Ils sont beaucoup plus larges que les registres classiques et mémorisent 2, 4, 8 entiers/flottants. * La seconde distinction est que les calculs SIMD sont effectués dans une unité SIMD séparée de l'ALU ou de la FPU. L'unité de calcul SIMD peut exécuter au minimum des instructions SIMD verticales, plus rarement des instructions SIMD horizontales. Pour ce faire, elle regroupe plusieurs additionneurs/multiplieurs, qui effectuent des calculs en parallèle. ===Les instructions SIMD verticales : plusieurs ALU travaillant en parallèle=== [[File:SIMD2.svg|vignette|Parallélisme de données au niveau de l'unité de calcul. Celle-ci contient plusieurs circuits indépendants qui appliquent la même opération sur des données différentes.]] Une unité de calcul SIMD gère au minimum des instructions SIMD verticales. Et pour ce faire, elle contient plusieurs additionneurs/multiplieurs séparés. Par exemple, pour additionner deux vecteurs contenant chacun 16 entiers/flottants, il faut utilise 16 additionneurs entiers et 16 additionneurs flottants. Dans le cas général, une ALU SIMD est composée de plusieurs ALU entières et flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. Notons que les ALU travaillent en parallèle, elles font des calculs indépendants. Notons que les additionneurs dans chaque ALU doivent pouvoir être configurés de manière à gérer des tailles de données différentes. Par exemple, si on prend un vecteur simple, qui peut contenir soit 32 entiers de 16 bits, soit 16 entiers 32 bits, soit 8 entiers 64 bits, alors on doit utiliser 8 additionneurs, mais chacun d'entre eux doit pouvoir être reconfiguré de manière à ne pas propager les retenues au-delà des premiers 16 ou 32 bits. Un défaut de cette organisation est que le cout en circuits est loin d'être négligeable. Il faut dupliquer des unités de calcul et les coller en rajoutant des circuits, cela utilise beaucoup de transistors. Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures. Retenez cependant : l'usage de plusieurs ALU travaillant en parallèle a plusieurs défauts. Premièrement, cela implique des vecteurs de taille fixe, du fait que le nombre d'ALU travaillant en parallèle est fixe. Deuxièmement, des difficultés d'implémentation concernant les instructions de réduction. Détaillons ce second point ! Une autre possibilité est d'utiliser moins d'unités de calcul qu'il n'y a d'éléments dans un vecteur. Par exemple, pour des vecteurs contenant 32 flottants, il se peut qu'il n'y ait que 16 ALU. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale. L'avantage est que l'ensemble des calculs se fait en une seule instruction machine. L'implémentation est généralement la suivante : le processeur décode une instruction SIMD en deux micro-instructions SIMD plus courtes. Par exemple, pour une instruction SIMD de 32 éléments, elle sera décodée en deux instructions SIMD de 16 éléments, chacune étant exécutée sur l'ALU. Pas mal de processeurs x86 font cela pour les instructions AVX 512, qui travaillent sur des vecteurs de 512 bits. Les premières version de l'AVX travaillaient sur des vecteurs de 256 bits, l'AVX-512 étendit le tout à 256 bits. Lors de l'introduction de l'AVX, la plupart des CPU x86 avaient une ALU 256 bits et les concepteurs de processeurs ont préféré la garder telle quelle. Les ALU SIMD travaillent donc sur des vecteurs de 256 bits, les instructions 256 bits étaient traitées comme une seule µops et les instructions 512 bits avec deux µops de 256 bits. Il est possible de réduire fortement l'impact en performance en doublant la fréquence de l'ALU. Par exemple, pour des vecteurs contenant 32 flottants, le processeur incorpore 16 ALU, mais celles-ci fonctionne à une fréquence double de celle du processeur. L'usage d'une ALU à double fréquence est une technique qui a été utilisée sur le Pentium 4 et quelques processeurs, pour des instructions non-SIMD. Mais pour les processeurs SIMD, elle s'applique à la perfection. L'implémentation est la même que précédemment, sauf que le ''scheduler'' fonctionne à double fréquence. Pour limiter la casse, il est préférable d'utiliser un ''scheduler'' séparé pour les instructions SIMD, qui sera seule à fonctionner à double fréquence. ===Les instructions horizontales : une ALU simple séparée=== Les instructions horizontales posent des difficultés d’implémentation. Elles ne peuvent pas s'implémenter en utilisant plusieurs ALU travaillant en parallèle, contrairement aux instructions verticales, ce qui fait qu'elles utilisent généralement des unités de calcul spécialisées. Et c'est là que la différence entre instructions de manipulation de vecteurs et instructions de réduction vient encore une fois poser problème. Les instructions de manipulation de vecteur sont relativement simples à implémenter : elles ont juste besoin d'une ALU spécialisée, relativement simple, composée de beaucoup de multiplexeurs à configurer convenablement. Mais pour les instructions de réduction, c'est autre chose ! L'implémentation des instructions de réduction est possible mais a un cout en circuits assez prohibitif. Pour additionner N entiers, il faut utiliser un additionneur multi-opérande qui prend beaucoup de circuits et dont le temps de calcul est proche de celui d'une multiplication (très lent). Trouver le maximum et le minimum d'un nombre demandent de faire la même chose avec des comparateurs. Le tout a un cout en circuit non-négligeable, pour accélérer des opérations assez peu fréquentes. Elles sont surtout utilisées pour trouver la somme ou le maximum/minimum d'un tableau, opération séquentielle par nature, avec peu parallélisme de données, peu courante. Une autre solution utilise une unité SIMD normale, mais intègre un système de contournement, de ''bypass'', qui relierait l'entrée d'une ALU à la sortie d'une autre. Mais le cout lié aux interconnexions est très important : on doit relier chaque ALU à toutes les autres dans le pire des cas, on peut optimiser le tout et réduire un peu la quantité d'interconnexions, mais le cout reste important. Au final, on gagne en circuits ce qu'on perd en interconnexions. Le choix entre les deux solutions est loin d'être facile et dépend du processeur, de la technologie utilisée, du budget en transistors disponibles, etc. Les processeurs SIMD purs résolvent ce problème assez simplement : ils n'implémentent pas d'instructions de réduction. Par contre, ils implémentent systématiquement des instructions de manipulation de vecteur, comme des permutations ou autres. La raison est que l'on peut émuler les instructions de réduction en utilisant des instructions SIMD de manipulation de vecteur couplées à des additions/multiplications SIMD verticales. Il s'agit donc d'un compromis entre cout en circuits et performance finale. Niveau circuits, on a une unité SIMD avec plusieurs ALU de calcul en parallèle, une unité de manipulation de vecteur séparée assez simple et au cout en circuits raisonnable. Les performances pour les opérations de réduction sont acceptables, le cout en performance est relativement modéré. ===Le banc de registre vectoriel=== La présence de registres vectoriels séparés permet d'avoir des vecteurs assez longs. Pour ce faire, les registres vectoriels sont plus grands que les registres généraux. Par exemple, sur un processeur 32 bits, qui gère donc des entiers de 32 bits, les vecteurs peuvent faire 128 ou 256 bits. Un vecteur contient donc plusieurs entiers ou flottants de taille maximale. On n'est pas dans le cas précédent, où registres généraux et SIMD sont les mêmes, ils sont séparés et n'ont pas la même taille. Le processeur utilise un banc de registre séparé pour les registres SIMD, appelé le '''banc de registre SIMD'''. Vu que les registres vectoriels sont assez longs, le banc de registre vectoriels a une grande taille, une grande capacité mémoire. Pour donner un exemple, les extensions SSE des processeurs x86 définissent 8 registres SIMD de 128 bits chacun. Ils ont été introduits à une époque où les processeurs avaient des registres généraux de 32 bits. Le banc de registres associé était donc environ 4 fois plus grand que celui pour les registres généraux. Je dis environ car le décodeur avait sensiblement la même taille, seul le plan mémoire était grossit, mais l'idée est là. Une conséquence est que le banc de registres SIMD consomme beaucoup d'énergie et d'électricité. Il en est de même avec les ALU SIMD. Et diverses optimisations tentent de réduire ce cout en énergie. L'une d'entre ajoute une mémoire cache entre les ALU SIMD et les registres vectoriels ! L'idée est que si un registre vectoriel est relu plusieurs fois de suite, la première lecture lira le vecteur dans le banc de registres SIMD, mais les lectures suivantes liront le vecteur dans le cache. L'optimisation marche car accéder au banc de registre vectoriel est vraiment couteux, plus qu'accéder à un cache. Le cache en question est appelé un '''''register reuse caches'''''. Les ''register reuse caches'' sont présents sur le processeur Monaka de Fujitsu, mais aussi et surtout dans les cartes graphiques modernes. Nous ferrons l'impasse sur les GPU modernes, car leur usage est quelque peu particulier. Il ne servent pas que à réduire la consommation d'énergie, mais aussi à réduire les conflits d'accès au banc de registre, qui est en réalité composé de plusieurs banques indépendantes. Leur usage sur le CPU Monaka est plus simple : ils ne servent vraiment qu'à réduire la consommation d'énergie, rien de plus. : Pour ceux qui veulent en savoir plus pour leur utilisation sur les cartes graphiques, tout est détaillé dans mon wikilivre sur les cartes graphiques, via le lien suivant. Mais sachez cependant qu'il faut idéalement lire le chapitre entier, et peut-être même les chapitres précédents, pour comprendre cette section. Voici le lien : [https://fr.wikibooks.org/wiki/Les_cartes_graphiques/La_microarchitecture_des_processeurs_de_shaders#L'Operand_Collector_et_les_caches_de_register_reuse L'Operand_Collector_et_les_caches_de_register_reuse] ===L'implémentation des LOAD-STORE à des données consécutives=== Les architectures SIMD pures sont des architectures de type LOAD-STORE, ce qui veut dire que les instructions SIMD ne peuvent que lire ou écrire dans les registres vectoriels, pas en mémoire ou ailleurs. Les transferts entre registres SIMD et mémoire sont le fait d'instructions LOAD et STORE spécialisées, qui transfèrent des vecteurs entre RAM et registres vectoriels. Les autres architectures n'ont pas nécessairement ce genre de restrictions, mais laissons cela pour plus tard. Implémenter les instructions LOAD/STORE pour des vecteurs dont les données sont consécutives en mémoire n'est pas très compliqué. Il suffit juste d'élargir le port de lecture/écriture du cache L1 de données, pour pouvoir lire/écrire un vecteur entier. Rien de bien compliqué, il suffit juste d'ajouter des interconnexions et d'ajouter des multiplexeurs. Du moins, c'est le cas tant que les vecteurs sont plus petits qu'une ligne de cache, ce qui est souvent le cas en pratique. Dans le cas très rare où les vecteurs sont plus longs qu'une ligne de cache, les LOAD/STORE doivent se faire en deux accès dans le cache, ce qui demande d'ajouter du matériel pour contrôler la situation. Précisons cependant qu'il s'agit là d'un cas idéal, où les vecteurs sont correctement alignés en mémoire. En effet, il se peut qu'un vecteur soit à cheval sur deux lignes de cache, alors qu'il est plus petit qu'une ligne de cache. Il suffit pour cela qu'il soit placé à une adresse adéquate. Par exemple, prenons un vecteur de 16 octets et des lignes de cache de 32 octets. Si le vecteur démarre à l'adresse 24, alors il sera à cheval sur deux lignes de cache. Il faut donc intégrer des circuits pour gérer cette situation. Pour cela, il suffit d'ajouter un registre pour stocker la première de ligne lue, puis un circuit qui combine les deux lignes de cache pour donner le vecteur final. ===L'implémentation des LOAD-STORE en ''scatter-gather''=== L'implémentation des accès en ''scatter-gather'' est plus compliquée. Un accès en ''scatter-gather'' demande de faire des calculs d'adresse, suivis par un ensemble de lectures/écritures, suivis par un regroupement des données lues dans un vecteur. Le calcul d'adresse peut se faire en parallèle dans une unité de calcul SIMD, dans plusieurs unités de calcul d'adresse en parallèle. Par contre, les lectures/écritures ne le peuvent pas. Le cache n'a pas assez de ports de lecture/écriture pour lire/écrire N données. Il a quelques ports, ce qui lui permet de lire/écrire 3/4 données grand maximum, soit moins que ce qui est nécessaire pour faire un accès en ''scatter-gather'' en une fois. Les lectures/écritures sont donc effectuées en plusieurs fois. Pour une opération de ''Gather'', il faut aussi regrouper les données lues dans un vecteur. Sur les caches des processeurs à haute performance, le calcul d'adresse est intégré dans le décodeur. Ce sont des caches adressés par somme, que nous avions abordé dans le chapitre sur les mémoires caches. Avec de telles caches, on ne peut pas utiliser une unité de calcul d'adresse SIMD, on ne peut calculer qu'autant d'adresse qu'il y a de ports sur le cache. Les accès en ''scatter-gather'' regroupent plusieurs accès mémoire, qui peuvent être effectués dans le désordre. La seule exception est celle où un ''scatter'' effectue plusieurs écritures à la même adresse, où les deux écritures doivent se faire dans l'ordre allant du premier au dernier élément du vecteur d'adresse. Les processeurs SIMD profitent de cette absence d'ordre pour effectuer les accès dans un ordre le plus optimisé possible. Par exemple, imaginons le cas où une instruction ''gather'' lise des éléments placés dans deux lignes de cache uniquement, mais dans le désordre en passant sans cesse d'une ligne de cache à l'autre. Il est alors possible de faire tous les accès dans la première ligne de cache en premier, avant de faire ceux allant dans l'autre. Une telle optimisation marche aussi bien pour les lectures que les écritures, pour les ''gather'' que pour les ''scatter''. Elle peut aussi être adaptée pour plus de deux lignes de cache. Elle porte le nom de '''''memory coalescing'''''. Une autre optimisation possible est de détecter le cas où un accès en ''scatter-gather'' accède à des données consécutives. Il suffit pour cela que les indices/adresses du vecteur opérande soient consécutives, et cela arrive plus souvent qu'on ne le pense. Dans ce cas, l'accès est remplacé par une instruction LOAD/STORE SIMD normale, beaucoup plus simple et plus rapide. Les deux optimisations demandent que les adresses soient calculées avant de faire l'accès mémoire. Les adresses calculées sont alors comparées entre elles, par un réseau de comparateurs assez complexe. Un point important de l'implémentation des instructions ''gather'' est qu'une donnée chargée doit être insérée au bon endroit dans le vecteur destination. Et pareil pour les ''scatter'' : il faut lire la bonne donnée au bon moment. Si on prend une ligne de cache complète, il faut lire plusieurs élèments en même temps et les envoyer au registre de destination au bon endroit. Il faut pour cela tout un réseau de multiplexeurs assez complexe pour faire ce travail. Il y a le même genre de réseau pour les instructions ''scatter'', mais qui va dans l'autre sens : du registre vers la ligne de cache. ==Les processeurs SIMD avec prédication== Un obstacle très gênant à la vectorisation est la présence de branchements conditionnels dans les boucles à vectoriser. Si une boucle contient des branchements conditionnels, elle ne peut pas être vectorisée facilement : il est impossible de zapper certains éléments d'un vecteur suivant une condition. Il n'est pas possible d'effectuer une instruction SIMD seulement sur certains éléments d'un vecteur et d'ignorer les autres, en fonction du résultat d'un branchement conditionnel. Tout le vecteur y passe, ou le vecteur est épargné, mais la condition intermédiaire n'est pas possible avec du SIMD simple. Pour résoudre ce problème, les processeurs SIMD incorporent diverses techniques pour implémenter ou éviter les branchements conditionnels le plus possible. La plus simple est l'incorporation de certaines instructions SIMD simples, comme la valeur absolue, le calcul du minimum/maximum, etc. Ces opérations sont généralement émulées en utilisant des branchements, avec quelques conditions simples. Incorporer ces instructions permet ainsi de faire disparaitre une partie des branchements du code. Elle ne paye pas de mine, mais elle a un résultat pas négligeable pour le rendu graphique, certaines applications de calcul scientifiques, et quelques autres. Son cout en transistors est très variable, il faut rajouter des circuits dans le séquenceur, l'unité de calcul SIMD est assez peu modifiée (il faut juste rajouter des multiplexeurs). Le vrai problème est que cette solution laisse énormément de branchements dans le code. Et qu'il faut donc trouver une solution plus générale, capable de réduire drastiquement les branchements. Pour cela, on réutilise une technique vue dans les chapitre sur l'assembleur et le langage machine : la prédication. ===Les instructions à prédicats=== Pour optimiser le cas général, il est possible d'utiliser des instructions à prédicats adaptées pour fonctionner sur des vecteurs. L'idée est que quand une instruction effectue un calcul sur un ou deux vecteurs, certains éléments du vecteurs sont ignorés. Les éléments à ignorer sont choisis suivant le résultat d'une instruction de comparaison, qui effectue un test : les éléments pour lesquels ce test est respecté sont pris en compte, ceux qui ne passent pas le test sont ignorés. Pour donner un exemple d'utilisation, imaginons que l'on ait un vecteur dans lequel on veut remplacer toutes les valeurs négatives par des 0. Dans ce cas, on utilise : * une instruction de comparaison, qui compare chaque élément du vecteur avec 0 et génère plusieurs bits de résultat ; * suivi d'une instruction à prédicat qui met à zéro les éléments pour lesquels les bits de résultat précédents sont à 1. La prédication peut aussi être utilisée pour simuler des vecteurs de taille variable. Tous les vecteurs ont une taille fixe, mais on peut ne pas faire de calculs sur la fin d'un vecteur, ce qui fait que tout se passe comme si le vecteur était plus petit. Ce n'est pas sa seule utilisation, mais la prédication permet de simuler ce comportement. Néanmoins, cela n'en fait pas un processeur vectoriel, capable de traiter des vecteurs de taille variable : il n'y a pas de moyen de préciser explicitement la taille des vecteurs à traiter. Tout cela demande plusieurs modifications du jeu d'instruction : ajouter des instructions de comparaison SIMD, ajouter de quoi faire la prédication. ===Le calcul des masques et le registre de masque=== La prédication demande que le résultat d'une comparaison soit stocké dans un registre à prédicat ou dans le registre d'état. Pour les processeurs SIMD, il y a cependant une grosse adaptation à faire concernant le fonctionnement des comparaisons et le stockage des résultats. Premièrement, les instructions de comparaison SIMD comparent une paire de deux éléments à la même place dans deux vecteurs et fournissent un résultat d'un bit pour chaque paire. Le résultat est donc un ensemble de bits, qui sont regroupées dans un vecteur spécialisé. Deuxièmement, reste à savoir que faire de ce vecteur. Pour cela, deux solutions sont possibles. Avec la première, le vecteur est stocké dans un registre vectoriel/SIMD comme les autres, il est traité comme n'importe quel autre vecteur. Avec la seconde solution, il reçoit un registre dédié, qui est un mix entre registre d'état et registre à prédicat, appelé un '''registre de masque''' (''Vector Mask Register''). Il stocke un bit pour chaque donnée présente dans le vecteur à traiter, qui indique s'il faut ignorer la donnée ou non. L'instruction SIMD suivante fait usage de ce masque pour savoir s'il faut faire l'opération pour chaque élément du vecteur. [[File:Vector mask register.png|centre|vignette|upright=2|Vector mask register]] Il peut y avoir un ou plusieurs registres de masque. S'il y en a plusieurs, ils sont généralement nommés, sur le modèle des registres à prédicats. L'application d'un masque demande de fournir le nom du registre de masque à utiliser, ils adressés explicitement dans les instructions. S'il n'y en a qu'un seul, il est possible de le pas le nommer et de faire en sorte qu'il soit adressé implicitement par les instructions qui en ont besoin. On parle alors de '''masquage implicite'''. Le masquage implicite est très utilisé sur les cartes graphiques, qui sont actuellement des architectures SIMD, comme nous le verrons plus bas. Un gros problème du masquage implicite est qu'il introduit des dépendances implicites entre instructions, qui ne sont pas explicites quand on regarder uniquement les registres manipulés par une instruction, qui perturbent le renommage de registres. Rien d'insurmontable, mais cela peut poser des problèmes de performance et la résolution de ce problème a un cout en hardware. Mais ce n'est un problème que sur les architectures à exécution dans le désordre. Les architectures à émission dans l'ordre ne sont pas concernées, et c'est notamment le cas sur les GPU et les cartes graphiques modernes. Diverses optimisations sont possible avec un registre de masque. La première est de ne pas exécuter d'instruction si tous les bits du masque sont à 0. Dans ce cas, l'instruction SIMD n'a pas de calculs à faire, vu que tous les résultats sont masqués. Il est alors possible de ne pas exécuter l'instruction SIMD et de passer directement à la suivante. L'implémentation matérielle est très simple : une porte NOR qui détermine si le registre de masque contient la valeur 0. La sortie de cette porte NOR est envoyée au séquenceur d'instruction. Le décodeur est alors conçu pour zapper l'instruction si le registre de masque contient un zéro. ===Les accès mémoire en ''scatter-gather'' avec prédication=== Un problème avec les accès mémoire que certains accès peuvent déclencher des défauts de page ou d'autres formes d'exceptions (erreurs de protection mémoire, autre). Si l'accès mémoire se fait sans prédication, ces exceptions sont gérées dans l'ordre des accès mémoire, en commençant par le début du vecteur, ce qui rend l'implémentation assez facile. Mais avec la prédiction, il se peut qu'un accès masqué et censé être ignoré déclenche une exception matérielle. Et le résultat dépend de l'implémentation. Une première solution est d'effectuer l'accès mémoire, et de masquer ensuite les éléments à ignorer. Mais dans ce cas, les éléments masqués peuvent déclencher des exceptions, qui ne sont pas censées se produire. Une autre solution est de déterminer quelles sont les adresses qu'il faut lire ou écrire et ne pas faire les accès mémoire masqués. Une autre solution effectue tous les accès avant de savoir quels sont ceux à masquer, mais de ne pas déclencher d'exception avant qu'on sache quels sont les accès masqués. Une fois le masquage des accès effectués, le processeur exécute les exceptions adéquates. Il s'agit d'un comportement de masquage d'exception, très utilisé. L'implémentation est assez simple pour les accès mémoire contiguës, mais plus compliqué pour les accès en ''stride'' ou en ''scatter-gather''. Pour les accès contiguës, les seules exceptions pouvant survenir sont celles ayant lieu quand on passe d'une page mémoire à la suivante (au sens pagination, mémoire virtuelle). Et le hardware pour détecter un débordement de page est assez simple. Mais pour les accès en ''stride'' ou en ''scatter-gather'' demandent qu'oin vérifie les exceptions pour chaque élément du vecteur. ===L'implémentation matérielle=== L'implémentation matérielle est assez proche des processeurs SIMD purs. On retrouve plusieurs unités de calcul travaillant en parallèle, avec quelques circuits ajoutés pour gérer la prédication. Le cout supplémentaire en circuits est acceptable, les gains en performance associés sont très intéressants. Les circuits en plus, pour gérer la prédication, sont similaires à ceux utilisés pour la prédication sur les architectures non-SIMD, mais sont dupliqués. La seule innovation architecturale est l'implémentation du registre de masque. Tous les processeurs n'en utilisent pas, mais c'est la solution la plus performante. En effet, ce registre de masque est un peu l'équivalent du registre d'état pour les instructions SIMD, bien qu'il soit techniquement une concaténation de registres à prédicat non-nommés. Ils ont pour avantage de libérer des registres SIMD normaux tout en ayant un cout en matériel très limité. Un autre point est que pour gérer les masques, les instructions SIMD à prédicat doivent lire le masque. Sans registre de masque, en utilisant un registre SIMD normal, il faut rajouter un troisième port de lecture sur le banc de registre pour récupérer le masque, ce qui est couteux en matériel. Pas besoin avec un registre de masque, qui n'est connecté qu'à l'ALU, sur le même modèle que le registre d'état. Plus haut, on a vu que l'implémentation de l'ALU SIMD peut se faire en utilisant des moins d'ALU que prévu. Par exemple, pour des vecteurs de 32 éléments, on peut utiliser 16 ALUs (allant à double fréquence ou non). Une instruction SIMD est alors décodée en plusieurs micro-instructions SIMD consécutives, chacune traitant une partie d'un vecteur. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. L'avantage est que cela se marie bien avec l'abandon des opérations pour les masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit vérifier que les 8 bits de poids faible ainsi que de poids fort du registre de masque ne soient pas individuellement à zéro. Cela se fait en dupliquant la porte NOR évoquer plus haut, l'une pour la partie basse du registre et la seconde pour la partie haute. ==La prédication pour les structures de contrôle imbriquées== Au niveau du jeu d’instruction, les architectures SIMT implémentent de la prédication, sous une forme améliorée. Les processeurs SIMT actuels sont surtout utilisées sur les processeurs intégrés aux cartes graphiques. Et ces derniers gèrent très mal les branchements, et encore : beaucoup de cartes graphiques, même récentes, ne gèrent tout simplement pas les branchements. Elles doivent donc se débrouiller avec uniquement la prédication, là où les processeurs SIMD utilisent des branchements normaux en complément de la prédication. Insistons sur le fait que cet usage exclusif de la prédication n'est présent que sur une sous-partie des architectures SIMT, le seul exemple que l'auteur de ce wikilivre connait étant celui des cartes graphiques. Les architectures SIMT sans branchements doivent donc trouver des solutions pour gérer les structures de contrôle imbriquées, à savoir une boucle placée à l'intérieur d'une autre boucle, un IF...ELSE dans un autre IF...ELSE, etc. Elles utilisent pour cela la prédication, combinée avec des mécanismes annexes. Le premier d'entre eux est l'usage de plusieurs registres de masques organisés d'une manière bien précise, l'autre est l'usage de compteurs d'activité. Voyons ces deux techniques. ===La pile de masques=== La '''pile de masques''' remplace le ou les registres de masque. Sans elle, le processeur SIMD incorpore un registre de masque qui est adressé implicitement ou explicitement. Éventuellement, le processeur peut contenir plusieurs registres de masque séparés adressables via un nom de registre. Avec elle, le processeur SIMD incorpore plusieurs registres de masque organisé en pile. Le registre de masque est donc remplacé par une mémoire LIFO, une pile, dans laquelle plusieurs masques sont empilés. Le tout forme une pile, similaire à la pile d'appel, sauf qu'elle est utilisée pour empiler des masques. Un masque est calculé et empilé à chaque entrée dans une structure de contrôle, puis dépilé une fois la structure de contrôle exécutée. L'empilement et le dépilement des masques est effectué par des instructions PUSH et POP, présentes dans le jeu d'instruction du processeur SIMD. Le calcul des masques doit répondre à plusieurs impératifs. * Premièrement, chaque masque se calcule en faisant un ET entre le masque précédent et le masque calculé par l'instruction de test. Cela permet de ne pas réveiller d’élément au beau milieu d'une structure imbriquée. Si in IF désactive certains éléments du vecteur, une condition imbriquée dans ce IF ne doit pas réveiller cet élément. Le fait de faire un ET entre les masques garantit cela. * Deuxièmement, les masques doivent être empilés et dépilés correctement. Au moment de rentrer dans une structure de contrôle, on effectue une instruction de test associée à la structure de contrôle, qui calcule un masque, et on empile le masque calculé. Au moment de sortir de la structure de contrôle, on dépile le masque en question. L'implémentation demande d'utiliser une mémoire LIFO pour stocker la pile de masques, et quelques circuits annexes. Il faut notamment un circuit relié à l'ALU qui récupère les conditions, les résultats des comparaisons, et qui effectue le ET pour combiner les maques. Pour donner un exemple, prenons le code suivant, qui est volontairement simpliste et ne sert qu'à des fins d'explication : <syntaxhighlight lang="c"> if ( condition 1 ) { if ( condition 2 ) { ... } else { ... } Autres instructions } Instructions après le IF... </syntaxhighlight> Imaginons que l'on traite des vecteurs de 8 éléments. Pour le vecteur considéré, la première condition (a > 0) n'est respectée que par les 4 premiers éléments. L'instruction de condition calcule alors le masque correspondant : 1111 0000. Le masque est alors calculé, puis empilé au somment de la pile. La seconde instruction de test, qui teste la variable b, est maintenant valide pour les 4 bits du milieu du masque. Mais n'allez pas croire que le masque correspondant soit 0011 11100 : il faut tenir compte de la condition précédente, qui a éliminé les 4 derniers éléments. Pour cela, on fait un ET logique entre le masque précédent, et le masque calculé par la condition. Le masque au sommet de la pile est donc lu, combiné avec le masque calculé par l'instruction, ce qui donne le masque final. Le masque final est alors empilé au sommet de la pile. On exécute alors l'instruction du IF, en tenant compte du masque qui est au sommet de la pile. Si le IF était plus compliqué, toutes les instructions suivantes tiendraient compte du masque. En fait, le masque est pris en compte tant qu'il n'est pas dépilé. Une fois que le IF est terminé, le masque est dépilé. On passe alors au ELSE, et rebelotte. Le masque pour le ELSE est calculé en combinant le masque au sommet de la pile avec la condition du ELSE. Le masque au sommet de la pile est celui calculé à l'entrée du premier IF, pas le second qui a été dépilé. Les instructions du ELSE sont alors exécutées en tenant compte de ce masque. Une fois qu'elles sont toutes exécutées, le masque est dépilé. Puis vient l'exécution des instructions après le ELSE. Elles utilisent le masque empilé au sommet de la pile, qui correspond à celui à l'entrée du IF. Puis vient le moment d'exécuter les instructions après le IF : pas de masque, on exécute sur tout le vecteur. ===Les compteurs d'activité=== Une variante de la technique précédente remplace la pile de masques par des '''compteurs d'activité'''. La technique est similaire, si ce n'est qu'elle utilise moins de circuits. Avant , on avait une pile de masques de même taille, dont les bits sont à 0 ou 1 suivant que la condition est remplie. La pile de masque ressemble donc à ceci : {|class="wikitable" |- ! masque 1 | 1 || 1 || 1 || 1 |- ! masque 2 | 0 || 1 || 1 || 1 |- ! masque 3 | 0 || 1 || 1 || 1 |- ! masque 4 | 0 || 0 || 0 || 1 |- ! masque 1 | colspan="4" | vide |} Une manière équivalent de représenter cette pile de masque est de compter combien de bits sont à 0 dans chaque colonne. Attention : j'ai bien dit à 0 ! On obtient alors : {|class="wikitable" |- ! masque 1 | 3 || 1 || 1 || 0 |} Et c'est le principe caché derrière la techniques des compteurs d'activité. Chaque élément dans un vecteur, chaque place, se voit attribuer un compteur. Un compteur non-nul indique qu'il ne faut pas prendre en compte l’élément. Ce n'est qu'une fois que le compteur est nul que l'on effectue des opérations sur l’élément associé du vecteur. À chaque fois qu'on entre dans une structure de contrôle, on teste une condition sur chaque élément. Si la condition est respectée pour un élément, alors le compteur ne change pas. Mais si la condition n'est pas respectée, alors on incrémente le compteur associé. En sortant de la structure de contrôle, on décrémente le compteur associé. Notons que les compteurs qui n'ont pas été incrémenté en entrant dans la structure de contrôle ne sont pas décrémenté en sortant. En clair, là où on empilait/dépilait un masque, on se contente d'incrémenter/décrémenter un compteur. Utiliser un compteur en lieu et place d'une colonne entière dans la pile de masque utilise moins de bits. Et c'est sans doute pour cette raison que certaines cartes graphiques, comme les cartes graphiques intégrées d'Intel depuis 2004, utilisent cette technique. ==Les processeurs vectoriels== Les '''processeurs vectoriels''' sont les ancêtres des processeurs à instruction SIMD. Bien qu'ils soient arrivés en premier, ils incorporent diverses techniques que de simples instructions SIMD n'ont pas forcément. C'est paradoxal, mais c'est ainsi. La première différence se manifeste au niveau du jeu d'instruction. Les processeurs vectoriels sont capables de traiter des vecteurs de taille variable, alors que les instructions SIMD usuelles ne gèrent que des vecteurs de taille fixe. Par taille variable, on veut dire que le processeur gère nativement des vecteurs d'une taille allant de 1 à la taille maximale d'un vecteur. La taille maximale d'un vecteur est celle permise par la taille des registres vectoriels, il s'agit de la taille d'un vecteur SIMD équivalent sur les autres processeurs SIMD. La seconde différence est une différence en termes de micro-architecture. Un processeur SIMD actuel dispose d'une unité de calcul très élaborée, capable de faire plusieurs calculs en parallèle, au moins un pour chaque élément du vecteur. Par exemple, si un vecteur contient 16 entiers, l'ALU SIMD doit contenir au moins 16 additionneurs entiers. Mais sur un processeur vectoriel, ce n'est pas le cas : l'unité de calcul est en réalité une unité de calcul entière/flottante normale, mais qui a la particularité d'être pipelinée. Les calculs sont donc effectués en parallèle, mais d'une manière totalement différente. La plupart de ces différences s'expliquent par le fait que les anciens processeurs vectoriels devaient faire avec des limitations en termes de circuits. Ils avaient peu de transistors à leur disposition, ce qui fait que leur unité de calcul devait être la plus simple possible. D'où l'utilisation d'une unité de calcul simple, mais utilisée de manière pipelinée. De plus, les processeurs vectoriels ne possèdent aucune mémoire cache pour les données et se contentent juste de caches d'instruction. De tels caches sont généralement peu utiles quand on manipule des tableaux, chose quasiment systématique quand on travaille avec du parallélisme de données. ===Une unité de calcul pipelinée=== La différence entre processeur vectoriel et SIMD tient dans la façon dont sont traités les vecteurs : les instructions SIMD traitent chaque élément en parallèle, alors que les processeurs vectoriels pipelinent ces calculs ! Par pipeliner, on veut dire que l’exécution de chaque instruction est découpée en plusieurs étapes indépendantes. Au lieu d'attendre la fin de l’exécution d'une opération avant de passer à la suivante, on peut commencer le traitement d'une nouvelle donnée sans avoir à attendre que l'ancienne soit terminée. [[File:Pipeline.png|centre|vignette|upright=2|Pipeline vectoriel.]] Pour donner un exemple, on peut donner l'exemple d'une multiplication flottante effectuée entre deux registres. Son exécution peut être décomposée en plusieurs étapes. Par exemple, on peut avoir 3 étapes : * une première étape E qui va additionner les exposants et gérer les diverses exceptions ; * une étape M qui va multiplier les mantisses ; * et enfin une étape A qui va arrondir le résultat. L’exécution de notre opération flottante sur un vecteur donnerait donc quelque chose dans le genre, où chaque ligne correspond au traitement d'un nouvel élément dans un vecteur, dans un paquet SIMD. [[File:Multiplication vectorielle - pipeline.png|centre|vignette|upright=2|Multiplication vectorielle - pipeline]] : Certains processeurs vectoriels utilisent plusieurs ALU pipelinées pour accélérer les calculs. On peut les voir comme des intermédiaires entre processeur vectoriels et SIMD SWAR. Avec une unité de calcul pipelinée découpée en N étages, on peut gérer N données simultanées : autant qu'il y a d'étapes différentes. Mais ce nombre maximal de données met un certain temps avant d'être atteint. L'unité de calcul met du temps avant d'arriver à son régime de croisière. Durant ce temps, elle n'a pas commencé à traiter suffisamment d’éléments pour que toutes les étapes soient occupées. Ce temps de démarrage est strictement égal du nombre d'étapes nécessaires pour effectuer une instruction. La même chose arrive vers la fin du vecteur, quand il ne reste plus suffisamment d’éléments à traiter pour remplir toutes les étapes. [[File:Startup and dead time vector pipeline.png|centre|vignette|upright=2|Startup and dead time vector pipeline]] Pour amortir ces temps de démarrage et de fin, certains processeurs démarrent une nouvelle instruction sans attendre la fin de la précédente : deux instructions peuvent se chevaucher pour remplir les vides. Par contre, il peut y avoir des problèmes lorsque deux instructions qui se suivent manipulent le même vecteur. Il faut que la première instruction ait fini de manipuler un élément avant que la suivante ne veuille commencer à le modifier. Pour cela, il faut avoir des vecteurs suffisamment qui contiennent plus d’éléments qu'il n'y a d'étapes pour effectuer notre instruction. ===La technique du ''chaining''=== La technique du pipeline peut encore être améliorée dans certains cas particuliers. L'idée est simplement d'ajouter la technique du contournement (''bypass'') à l'unité de calcul. Pour rappel, le contournement connecte la sortie de l'ALU à une de ses entrées, ce qui permet au résultat d'un calcul d'être réutilisable au cycle d’horloge suivant. L'usage du contournement sur les processeurs vectoriels porte le nom de '''''Vector Chaining'''''. Un processeur implémentant le ''chaining''/''bypass'' a toutes ses unités de calcul reliées entre elles : la sortie d'une unité est reliée aux entrées de toutes les autres. Pour comprendre à quoi peut servir cette technique, on peut citer deux grands exemples principaux. Le premier avantage est que l'implémentation des instructions SIMD horizontales est beaucoup plus simple. C'est pour cela que les processeurs vectoriels intègrent souvent des instructions horizontales, et notamment des instructions arithmétiques horizontales. Le contraste avec les processeurs SIMD récents, qui utilisent plusieurs ALU travaillant en parallèle, est frappant. Ces derniers n’intègrent pas d'instructions horizontales arithmétiques, les seules opérations horizontales supportées sont généralement des permutations ou des instructions ''compress/expand'', guère plus. Maintenant, introduisons une autre possibilité par un exemple. Dans cet exemple, on suppose que le processeur dispose d'une ALU pour l'addition et d'une autre pour la multiplication. Imaginons que l'on ait trois vecteurs nommés A, B et C. Pour chaque énième élément de ces paquets, je souhaite effectuer le calcul <math>A_n + B_n \times C_n</math>. En théorie, il faudrait faire d'abord la multiplication, stocker le résultat temporaire de la multiplication dans un registre vectoriel, puis faire l'addition. Mais en rusant un peu, on peut utiliser le pipeline plus efficacement. Une fois que le premier élément de la multiplication du premier vecteur est connu, pourquoi ne pas démarrer l'addition immédiatement après, et continuer la multiplication en parallèle ? Après tout, les deux calculs ont lieu dans des ALUs séparés. Et le contournement permet d'alimenter l'ALU d'addition avec la sortie du circuit multiplieur. [[File:Vector chaining.png|centre|vignette|upright=2|Vector chaining]] ===La gestion des vecteurs de taille variable=== L'usage d'une unité de calcul pipelinée fait que la taille des vecteurs n'est pas forcément fixe. Autant les processeurs SIMD non-vectoriels disposent d'un nombre fixe d'unités de calcul, et peuvent donc gérer des vecteurs de taille fixe, autant ce n'est pas le cas des processeurs vectoriels. Une unité de calcul pipelinée à 16 étage peut utiliser les trois premiers pour traiter un vecteur de 3 élements, les 5 suivants pour un second vecteur de 5, et le reste pour un troisième vecteur. Reste que la gestion des vecteurs de taille variable doit être gérée au niveau du séquenceur, mais surtout : doit être permise au niveau du jeu d'instruction. Pour cela, il faut ajouter au jeu d'instruction de quoi indiquer la taille du vecteur en cours de traitement. Et c'est le rôle d'un registre spécialisé, le '''Vector Length Register''', que de gérer les vecteurs de taille variable. Le ''Vector Length Register'' est un registre qui indique combien d’éléments on doit traiter dans un vecteur. On peut ainsi dire au processeur : je veux que tu ne traites que les 40 premiers éléments présents d'un paquet. Ils sont utilisés pour gérer des tableaux dont la taille n'est pas un multiple d'un vecteur. Quand on arrive à la fin d'un tableau, il suffit de configurer le ''Vector Length Register'' pour ne traiter que ce qu'il faut. [[File:Vector length register.png|centre|vignette|upright=2|Vector length register.]] Il facilite l'usage du '''déroulage de boucle''', une optimisation qui vise à réduire le nombre d'itérations d'une boucle en dupliquant son corps en plusieurs exemplaires. Le compilateur réplique le corps de la boucle (les instructions à répéter) en plusieurs exemplaires dans cette boucle, avant de corriger le nombre d'itérations de la boucle. Par exemple, prenons cette boucle, écrite dans le langage C : <syntaxhighlight lang="c"> int i; for (i = 0; i < 100; ++i) { a[i] = b[i] * 7 ; } </syntaxhighlight> Celle-ci peut être déroulée comme suit : <syntaxhighlight lang="c"> int i; for (i = 0; i < 100; i+=4) { a[i] = b[i] * 7 ; a[i+1] = b[i+1] * 7 ; a[i+2] = b[i+2] * 7 ; a[i+3] = b[i+3] * 7 ; } </syntaxhighlight> Les instructions vectorielles permettent de traiter plusieurs éléments à la fois, ce qui fait que plusieurs tours de boucles peuvent être rassemblés en une seule instruction SIMD. Le déroulage de boucles permet d'exposer des situations où ce regroupement est possible. Dans notre exemple, si jamais notre processeur dispose d'une instruction de multiplication capable de traiter 4 éléments du tableau a ou b en une seule fois, la boucle déroulée peut être vectorisée assez simplement en utilisant une multiplication vectorielle (que nous noterons vec_mul). <syntaxhighlight lang="c"> int i; for (i = 0; i < 100; i+=4) { vec_a[i] = vec_mul ( vec_b[i] , 7 ) ; } </syntaxhighlight> Le déroulage de boucles n'est toutefois pas une optimisation valable pour toutes les boucles. Reprenons l'exemple de la boucle vue plus haut. Si jamais le tableau à manipuler a un nombre d’éléments qui n'est pas multiple de 4, la boucle ne pourra être vectorisée, vu que la multiplication vectorielle ne peut traiter que 4 éléments à la fois. Pour ce faire, les compilateurs utilisent généralement deux boucles : une qui traite les éléments du tableau avec des instructions SIMD, et une autre qui traite les éléments restants avec des instructions non vectorielles. Cette transformation s'appelle le '''strip-mining'''. Par exemple, si je veux parcourir un tableau de taille fixe contenant 102 éléments, je devrais avoir une boucle comme celle-ci : <syntaxhighlight lang="c"> int i; for (i = 0; i < 100; i+=4) { a[i] = b[i] * 7 ; a[i+1] = b[i+1] * 7 ; a[i+2] = b[i+2] * 7 ; a[i+3] = b[i+3] * 7 ; } for (i = 100; i < 102; ++i) { a[i] = b[i] * 7 ; } </syntaxhighlight> Les processeurs vectoriels utilisent le ''Vector Length Register'' pour éviter d'utiliser le strip-mining. Avec ce registre, il est possible de demander aux instructions vectorielles de ne traiter que les n premiers éléments d'un vecteur : il suffit de placer la valeur n dans le ''Vector Length Register''. Évidemment, n doit être inférieur au nombre d’éléments maximal du vecteur. Avec ce registre, on n'a pas besoin d'une seconde boucle pour traiter les éléments restants, et une simple instruction vectorielle peut suffire. L'avantage principal est que cela réduit la portion de code non-parallélisable, donc augmente l'efficience SIMD. Mais l'intérêt n'est pas qu'une question de code. Il faut aussi prendre en compte que cela permet d'utiliser l'ALU plus efficacement. Dès qu'un vecteur court est terminé, le processeur peut immédiatement démarrer le calcul d'un autre vecteur, le vecteur suivant, l'instruction suivante, sans laisser de cycles inutilisés. Avec un processeur SIMD non-vectoriel, on aurait du utiliser de la prédication, donc utiliser seulement une partie des ALU SIMD, ce qui aurait été un gâchis. ===Les accès mémoire=== Les accès mémoire des processeurs vectoriels sont sensiblement les mêmes que ceux des autres processeurs SIMD. Les instructions d'accès mémoire sont les mêmes, on retrouve les modes d'adressages précédents, dont les accès en ''scatter-gather'' et en ''stride''. Sur certains processeurs vectoriels, les instructions vectorielles lisent et écrivent en mémoire RAM, sans passer par des registres : on parle de '''processeurs vectoriels mémoire-mémoire'''. Elles n'avaient pas d'instructions LOAD et STORE, les instructions arithmétiques géraient directement la lecture des opérandes en mémoire, et incrémentaient automatiquement les adresses à lire/écrire. Elles pouvaient gérer des vecteurs de taille arbitraire, la gestion d'un tableau se faisait parfois en une seule instruction ! Le problème est que l'absence de registres pour stocker les données lues réduisait grandement les performances. [[File:Processeur vectoriel mémoire-mémoire.png|centre|vignette|upright=2|Processeur vectoriel mémoire-mémoire]] Les processeurs vectoriels mémoire-mémoire étaient cependant minoritaires, la grosse majorité des processeurs vectoriels récents possèdent des registres vectoriels dédiés aux vecteurs. La plupart sont des architectures de type LOAD-STORE, mais pas toute. De fait, il n'y a pas de lien strict entre adressage des opérandes et processeurs vectoriel (ou SIMT). Seuls les processeurs SIMD de type SWAR ont une restriction là-dessus et sont systématiquement des architectures LOAD-STORE. [[File:Processeur vectoriel à registres vectoriels.png|centre|vignette|upright=2|Processeur vectoriel à registres vectoriels.]] Il faut noter que l'implémentation des accès mémoire est potentiellement différente de celle des autres processeurs SIMD. En principe, on peut utiliser une implémentation identique entre les deux. Mais le fait que les processeurs vectoriels sont pipelinés fait qu'il est préférable d'utiliser une implémentation différente, qui se base sur une RAM ou des caches pipelinés. Vu que l'ALU reçoit une/deux opérandes par cycle, l'idéal est que la mémoire ou le cache aillent au même rythme et soient donc pipelinés.Bien sur, il est possible d'utiliser une implémentation plus classique, avec des caches multiports, un chargement en plusieurs fois des vecteurs depuis le cache, des circuits pour réorganiser les accès mémoire, etc. En théorie, les architectures vectorielles font peu usage de mémoires cache. Les transferts entre registres vectoriels et mémoire RAM sont directs, sans intermédiaire. La raison est que les applications qui tirent parti du SIMD ont une bonne localité spatiale, mais une mauvaise localité temporelle. Or, les caches de données profitent surtout de la localité temporelle pour donner de bonnes performances. De plus, rappelons que les processeurs vectoriels sont assez anciens et datent d'une époque où les transistors étaient précieux. Il valait mieux utiliser des transistors dans un gros cache d'instruction que pour un cache de donnée peu efficace. ==Les systèmes SIMD à plusieurs processeurs : le SIMT== Enfin, terminons avec le dernier type de SIMD, qui date du tout début de l'informatique : celui où on combine plusieurs processeurs non-SIMD. L'idée est de synchroniser plusieurs processeurs de manière à ce qu'ils exécutent la même instruction en même temps, chacun sur des données différentes. On parle d''''exécution ''lockstep''''' des instructions. Toute la difficulté est de garantir la synchronisation des processeurs, garantir que tous les processeurs exécutent la même instruction. Ce qui pose problème quand des branchements sont impliqués, des processeurs pouvant prendre le branchement et pas les autres. ===Les ''array processors'' des temps anciens=== Les premières architectures matérielles conçues pour le parallélisme de données étaient de ce type. On peut par exemple citer l'exemple des Thinking machines CM-1 et CM-2, qui exécutaient tous la même instruction au même cycle d'horloge sur 64000 processeurs minimalistes. Un autre exemple est celui de l'ILLIAC IV. Historiquement, le terme utilisé pour désigner de telles architectures était '''array processors''', encore que ce terme fût réservé aux systèmes multiprocesseurs. Par la suite, le terme ''Single Instruction Multiple Threads'', ou SIMT, a été utilisé. Mais ce terme a ensuite été repris par le marketing de NVIDIA pour désigner une technique précise de SIMD avec prédication, utilisée sur ses cartes graphiques récentes. Le mauvais usage de la terminologie fait que le terme SIMT est devenu polysémique et quelque peu trompeur. L'ILLIAC 4 était un ordinateur un peu particulier, presque unique en son genre. Son architecture était composé d'un processeur principal couplé à plusieurs processeurs secondaires. Il y avait en tout un processeur principal et 64 processeurs secondaires. Le processeur principal chargeait les instructions depuis la mémoire, les décodaient, puis envoyait les instructions aux processeurs secondaires. Le processeur principal était le seul à avoir un ''program counter'', il était utilisé pour gérer les boucles, ainsi que pour configurer les instructions à prédication. Cependant, le processeur principal n'était pas un simple séquenceur : il pouvait exécuter des instructions, avait un accumulateur et des registres, etc. Il prenait en charge toute instruction non-SIMD, alors que les instructions SIMD étaient envoyées aux processeurs secondaires. Les 64 processeurs secondaires avaient chacun : une unité de calcul, un ''local store'' de 2048 flottants de 64 bits, une unité mémoire. Ils n'ont pas de séquenceur ni de ''program counter'', ni de quoi charger une instruction. L'unité de calcul est appelée un ''processing element'' ou PE. Elle intègre un additionneur flottant, de quoi faire des décalages, et une unité de calcul logique. De plus, un PE intègre des registres accumulateurs et de quoi calculer des adresses : un registre d'indice, un registre d'adresse, un circuit de calcul d'adresse. L'unité mémoire connectait l'ALU entière PE et le ''local store'', et elle permettait aussi d'adresser des entrées-sorties. Pour gérer la prédication, les processeurs secondaires pouvaient être activés ou désactivés suivant le résultat des branchements/conditions. Intuitivement, on peut faire le rapprochement avec un processeur SIMD. Le processeur principal regrouperait la partie non-SIMD d'un processeur SIMD, alors que les processeurs secondaires seraient l'unité de calcul SIMD. Cependant, il y a une différence très importante : il n'y a pas de banc de registre vectoriel. À la place, chaque processeur secondaire a son propre banc de registre, et ce sont des registres scalaires ! Une autre différence est que la mémoire RAM est découpée en plusieurs ''local store'' de 2048 adresses, chacun étant relié à une unité de calcul PE. N'oublions pas le système de calcul d'adresse et l'unité LOAD/STORE qui est fusionnée avec l'unité SIMD, chaque PE ayant ses circuits de calcul d'adresse, sa propre unité mémoire, etc. ===Les anciennes cartes graphiques hybrides SIMD-VLIW=== Une technique de SIMD multi-processeur a été utilisée autrefois sur certaines cartes graphiques AMD à architecture Terascale. La différence est que l'architecture en question était multicœur et non multi-processeurs. Elles contenaient plusieurs cœurs VLIW, qui fonctionnaient en ''lockstep''. Un groupe de 16 processeurs VLIWs exécutaient la même opération VLIW. Du moins, c'est ce que semblent dire les [https://www.techpowerup.com/gpu-specs/docs/amd-gcn1-architecture.pdf white papers de présentation de l'architecture GCN]. L'usage de cet hybride SIMD-VLIW était très adapté au traitement graphique. Les cartes graphiques post-années 2000 sont capables d'exécuter des programmes appelés des '''''shaders''''', qui sont utilisés en rendu 3D, pour calculer certaines scènes 3D, et notamment pour calculer les ombres (d'où leur nom de shader, pour shade - ombre). Or, ces shaders sont justement exécutés par la carte graphique, sur les processeurs de shaders. Un processeur de ''shader'' applique un programme/''shader'' sur chaque ''pixel'' ou triangle d'une scène 3D (en fait des fragments et des vertices, mais faisons cette simplification). Chaque triangle ou pixel d'une scène 3D peut être traité indépendamment des autres, ce qui rend le traitement 3D fortement parallèle. Aussi, l'usage du SIMD est assez naturel : chaque processeur exécute un ''shader'' sur un pixel/triangle, utiliser une architecture SIMD parait naturel. Reste à justifier l'usage de plusieurs CPU VLIW. La raison tient dans la manière dont est traité un pixel par un shader. Un pixel est codé en utilisant quatre informations. Premièrement, une couleur codée avec trois nombres flottants, qui représentent une couleur au format RGB avec un mélange de rouge, vert et bleu, chaque couleur rouge/bleu/vert étant codée avec un flottant de 32 bits. La quatrième information est un nombre qui code la transparence du pixel. Généralement, le shader traite différemment la transparence de la couleur RGB, dans le sens où les instructions utilisées ne sont pas les mêmes. Au lieu de traiter couleur RGB et transparence séparément, elles sont traitées en même temps en parallèle, ce qui demande de pouvoir émettre deux instructions : une pour le calcul de la transparence, une autre pour la couleur RGB. Fait amusant, l'instruction utilisée pour gérer la composante RGB est une instruction vectorielle qui travaille sur des vecteurs de 3 couleurs. Les architectures VLIW sont parfaites pour cela. Les cartes graphiques de l'époque de DirectX 9 utilisaient cette méthode qui mélangeait SIMD et VLIW. Elles disposaient de plusieurs cœurs VLIW qui exécutaient le même shader en exécution ''lockstep''. Chaque cœur traitait au moins un pixel à la fois, ou un triangle à la fois. Cependant, nous devons préciser que les cœurs VLIW partagent la même unité de contrôle, ce sont en réalité des chemins de données plus qu'autre chose. L'architecture était aussi combinée avec du ''Fine Grained Multithreading'' afin de pouvoir gérer les accès mémoire, de telles architectures ne pouvant pas se permettre d'utiliser l'exécution dans le désordre. De telles architectures multicœurs à exécution ''lockstep'' permettent quelques optimisations, comme le fait de partager le cache d'instruction entre les cœurs. ==Résumé des différents types de processeurs SIMD== Dans ce chapitre, nous avons appris qu'il existe plusieurs catégories de processeurs SIMD. Les différences entre ces catégories tiennent à la fois dans le jeu d'instruction, mais aussi dans la microarchitecture des processeurs en question. Au niveau du jeu d'instruction, il y a trois paliers, qui sont résumés dans le tableau ci-dessous. {|class="wikitable" |- ! ! Support de la prédication ! Taille des vecteurs ! Instructions horizontales ! Architecture LOAD-STORE ! Pointeur de pile |- ! Processeurs SIMD purs | Non | rowspan="3" | Fixe | rowspan="3" | Limitées à des échanges de données à l'intérieur d'un vecteur, pas d'instructions de réduction | Oui | rowspan="2" | Unique |- ! Processeurs SIMD avec prédication | Oui, par définition | rowspan="3" | Dépend du processeur |- ! Processeurs SIMT | rowspan="2" | Oui, sauf exceptions | Un pointeur de pile possible par élément du vecteur |- ! Processeurs vectoriels | Variable | Toutes supportées, y compris les instructions arithmétiques horizontales | Unique |} Au niveau de la microarchitecture, il y a trois paliers, qui sont résumés dans le tableau ci-dessous. {|class="wikitable" |- ! ! Unité de calcul ! Banc de registre ! Adressage des opérandes |- ! Processeurs SIMD | rowspan="2" | Plusieurs ALU simples non-pipelinée, plusieurs additionneurs/multiplieurs/autres | Banc de registres vectoriels | Architecture LOAD-STORE |- ! Processeurs SIMT | Plusieurs bancs de registres scalaires (non-vectoriels, registres normaux) | rowspan="2" | Dépend du processeur |- ! Processeurs vectoriels | ALU simple unique, pipelinée | Banc de registres vectoriels |} Les processeurs SIMD purs ont des performances différentes des processeurs vectoriels, vu que les premiers font leurs calculs en parallèle, alors que les CPU vectoriels utilisent un pipeline. Mais dans les grandes lignes, les performances sont similaires. Les différences sont grandement atténuées par l'usage du ''vector chaining'', de la gestion des vecteurs de taille variables, et des autres optimisations spécifiques aux processeurs vectoriels. L'avantage est cependant du côté des processeurs SIMD pur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Architectures multithreadées et Hyperthreading | prevText=Architectures multithreadées et Hyperthreading | next=La cohérence des caches | nextText=La cohérence des caches }} {{autocat}} </noinclude> itn3idtrszd50vno16wbucd8kmeuyy3 773897 773896 2026-10-01T19:43:08Z Mewtow 31375 /* Le banc de registre vectoriel */ 773897 wikitext text/x-wiki Nous allons maintenant aborder le parallélisme de données, qui consiste à traiter des données différentes en parallèle. De nombreuses situations s'y prêtent relativement bien : traitement d'image, manipulation de sons, vidéo, rendu 3d, etc. Mais pour exploiter ce parallélisme, il a fallu concevoir des processeurs adaptés. Les architectures les plus simples exécutent une instruction sur plusieurs données en parallèle, à l'intérieur d'un processeur. Ces architectures exploitent le parallélisme de données au niveau de l'unité de calcul, celle-ci pouvant exécuter un même calcul sur des données différentes en parallèle. Si on omet quelques exceptions, on peut classer ces architectures dans plusieurs catégories principales : les processeurs à instructions SIMD, les processeurs à instructions SIMD à prédicat, les processeurs SIMT, les processeurs vectoriels. Si les processeurs vectoriels sont assez rares de nos jours, tous les processeurs récents incorporent des instructions SIMD. Quant au SIMT, il est utilisé sur les cartes graphiques modernes. ==Les instructions SIMD : les points communs entre tous les processeurs SIMD== Les processeurs modernes fournissent presque tous des '''instructions SIMD''', qui sont capables de traiter plusieurs éléments en parallèle. Elles travaillent sur des nombres entiers ou flottants regroupés dans ce qu'on appelle un ''vecteur'', qui sont eux-mêmes stockés dans des registres spécialisés. En général, tous les vecteurs ont une taille fixe, peu importe leur contenu. Cela implique que suivant la taille des données à manipuler, on pourra en placer plus ou moins dans un vecteur. Par exemple, un vecteur de 128 bits pourra contenir 4 entiers de 32 bits, 4 flottants 32 bits, ou 8 entiers de 16 bits. [[File:Vector register.png|centre|vignette|upright=2|Contenu d'un vecteur en fonction du type de données utilisé.]] Les vecteurs sont stockés dans des '''registres vectoriels''', aussi appelés '''registres SIMD'''. Un registre vectoriel peut contenir un vecteur complet, pas plus. En conséquence, ils ont une taille assez importante : ils font généralement 128,,256, voire 512 bits, comparé aux 32/64 bits des registres scalaires. Le résultat est que les processeurs SIMD modernes ont des registres SIMD séparés des registres entiers/flottants normaux. Un défaut de cette organisation est que les registres vectoriels sont des registres en plus, qui doivent être sauvegardés lors des commutations de contexte, lors des interruptions, lors des appels systèmes, etc. {| |+ Comparaison entre un processeur sans registres vectoriels, et avec registres vectoriels. |[[File:Non-SIMD cpu diagram1.svg|vignette|upright=1.5|CPU Non-SIMD]] |[[File:SIMD cpu diagram1.svg|vignette|upright=1.5|CPU SIMD]] |} Les instructions SIMD peuvent être rassemblées en deux grands groupes : les horizontales et les verticales. Les '''instructions SIMD verticales''' travaillent en parallèle sur les éléments qui sont "à la même place" dans deux vecteurs. Elles peuvent additionner ou multiplier deux vecteurs, par exemple. Pour prendre l'exemple d'une instruction d'addition vectorielle, celle-ci va additionner ensemble les données qui sont à la même place dans deux vecteurs, et placer le résultat dans un autre vecteur, à la même place. Les '''instructions SIMD horizontales''' partent d'un vecteur et ont pour résultat un simple nombre. Elles peuvent calculer la somme ou le produit des éléments d'un vecteur, renvoyer le nombre d’éléments nuls dans un vecteur, etc. [[File:Instructions SIMD.png|centre|vignette|upright=2.5|Instructions SIMD]] Les instructions SIMD sont difficilement utilisables dans des langages de haut niveau et c'est donc au compilateur de traduire un programme avec des instructions vectorielles. Les transformations qui permettent de traduire des morceaux de programmes en instructions vectorielles (déroulage de boucles, strip-mining) portent le nom de vectorisation. ===Les instructions SIMD arithmétiques verticales=== Les instructions SIMD sont essentiellement des instructions arithmétiques, comme des additions, des soustractions, des multiplications, éventuellement des opérations bit à bit, et quelques autres. Suivant la taille des données, et le type de celle-si, on devra effecteur des instructions différentes. Par exemple, on devra utiliser deux instructions d'addition différentes suivant qu'on manipule des flottants 64 bits ou des entiers 32 bits. De même, l'instruction pour additionner des vecteurs d'entiers de 16 bits sera différente de celle manipulant des vecteurs d'entiers de 32 bits. L'addition et la multiplication peuvent générer des débordements d'entier. Pour les instructions non-SIMD, le débordement d'une addition est géré par un bit de retenue, stocké dans le registre d'état. Mais pour les instructions SIMD, ce n'est pas une solution très facile à implémenter. À la place, les additions et soustractions utilisent généralement l'arithmétique saturée, à savoir que lorsqu'une addition déborde, le résultat est la valeur maximale représentable dans un entier. Pour la multiplication, les instructions non-SIMD génère un résultat qui est codé sur le double du nombre de bits. Une multiplication de deux nombres 64 bits donnera un résultat sur 128 bits, par exemple. Pour gérer cela, les multiplications non-SIMD stockent ce résultat dans deux registres, ou ne conservent que les 64 bits de poids faible. D'autres solutions sont possibles, mais ces deux là sont les plus utilisées. Pour les instructions SMID, une autre solution est préférée car plus simple pour de telles instructions : fournir deux instructions : une qui calcule les 64 bits de poids faible du résultat, une autre pour les 64 bits de poids fort. Les processeurs SIMD étant utilisés pour du traitement d'image ou du rendu 2D/3D, ils supportent généralement des opérations mathématiques assez complexes sur des nombres flottants. Il n'est pas rare d'avoir des instructions SIMD pour le calcul de l'inverse d'un nombre, de sa racine carrée, l'inverse d'une racine carrée, des fonctions trigonométriques, etc. L'implémentation matérielle de telles instructions est généralement très complexe et gourmande en circuits, ce qui fait que les concepteurs de processeurs rusent. Sauf sur certains processeurs assez peu fréquents, les instructions mathématiques complexes ne calculent pas un résultat exact, mais une approximation du résultat. Utiliser un résultat approximatif ne pose pas de problème pour de telles applications, le rendu d'image ou 3D s'en accommodant parfaitement, contrairement au calcul scientifique. ===Les instructions SIMD de manipulation de données intra-vecteur=== Après avoir vu les instructions SIMD verticales, il est temps de voir les instructions SIMD horizontales. Pour rappel, celles-ci travaillent un vecteur unique, dont elles réarrangent ou modifient les éléments. Tout cela sera plus parlant une fois qu'on aura donné quelques exemples. Mais avant toute chose, nous allons séparer les instructions horizontales en deux sous-types, fondamentalement différents. Le premier sous-type change de place les données dans un vecteur. Le second type est celui des instructions horizontales arithmétiques/logiques habituelles, qu'on verra plus tard. La distinction entre les deux est très importante, comme on le verra plus tard. [[File:SIMD instruction data movement - One operand.png|vignette|upright=1.5|Instructions SIMD de mouvement de données à une opérande.]] Les instructions SIMD horizontales les plus communes bougent des données à l'intérieur des vecteurs, ce qui leur vaut le nom d''''instructions de manipulation de vecteur'''. Les plus simples sont celles qui ont une seule opérande, à savoir qui prennent un vecteur comme opérande et renvoient un vecteur résultat. Les plus intuitives sont les instructions de '''permutation''', dont le nom est assez explicite. Elles changent de place des données dans un vecteur. Dans le cas le plus compliqué, elles prennent deux vecteur : le vecteur dans lequel faire la permutation, et un vecteur qui indique comment faire la permutation. Le vecteur précise, pour chaque élement du vecteur, où il doit aller après la permutation. L'instruction effectue la permutation des entiers/flottants/octets dans le vecteur. Les instructions '''''compress''''' et '''''expand''''' sont elles aussi spécifiques aux vecteurs. L'instruction ''compress'' prend un vecteur en opérande, sélectionne certaines valeurs présentes dedans, et les stocke dans un vecteur de sortie. Elle regroupe les valeurs sélectionnées dans le début du vecteur, et laisse les cases inoccupées à 0. Par exemple, pour un vecteur de 16 entiers, elle permet de sélectionner 5 entiers dedans, et les place dans un vecteur résultat qui ne contient que ces 5 valeurs au début du vecteur, le reste est à 0. L'instruction ''expand'' fait l'inverse : elle prend un vecteur crée par l'instruction ''compress'' (ou du moin qui a le même format), prend toutes les valeurs non-nulles dedans, et les disperse dans un vecteur de sortie, en les mettant à la place désirée. Les autres instructions SIMD horizontales prennent deux opérandes, voire aucune opérande vectorielle (elles prennent un nombre, pas un vecteur). Les instructions '''''shuffle''''' prennent plusieurs vecteurs, sélectionnent les éléments adéquats dans chaque vecteur, et les regroupe dans un vecteur résultat. Par exemple, prenons 2 vecteurs de 16 entiers chacun. Une instruction ''shuffle'' peut prendre 8 entiers dans chaque vecteur et les regrouper dans un vecteur résultat unique de 16 entiers. Ou encore, elle peut prendre trois vecteurs de 16 flottants chacun, prendre 4 flottants dans le premier, 10 dans le second et 2 dans le troisième, et regrouper le tout dans un vecteur résultat de 16 flottants. Il arrive que les deux cas précédents correspondent à des cas séparés, à deux instructions de type ''shuffle'', mais qui fonctionnent différemment. Le premier type prend les N premiers éléments du premier vecteur, puis prend les élements manquant à partir de la fin du vecteur. Le second type prend un élément sur deux dans chaque vecteur, mais avec un décalage d'un rang entre les deux vecteurs. Il y a aussi les instructions dites de '''''broadcast''''', qui copient une valeur unique dans un vecteur entier. Elles sont surtout utilisées pour initialiser des tableaux avec une valeur unique, ou pour remplir un vecteur avec des données prédéterminées pour certains calculs impliquant des constantes. [[File:SIMD instruction exemple.png|centre|vignette|upright=1.5|Instructions SIMD de mouvement de données qui ne sont pas à une opérande.]] ===Les instructions SIMD de réduction=== Il existe des instructions horizontales de type arithmétiques. La plus simple est celle qui additionne les différents entiers/flottants d'un vecteur. Elle est utilisée pour accélérer une opération précise : faire la somme d'un tableau d'entier/flottants. On peut aussi citer les opérations maximum et minimum, qui renvoient le plus grand/petit élément d'un vecteur. Un point important avec ces opérations est qu'elles prennent un vecteur, mais renvoie un résultat unique, un scalaire, un nombre entier/flottant seul. Elles sont parfois appelées des '''instructions de réduction'''. Les instructions de réduction sont généralement la spécificité des processeurs vectoriels, les processeurs SIMD récents n'en ont pas. La raison est que leur implémentation matérielle est généralement assez compliquée. Contrairement aux instructions verticales, elles ont une forme de parallélisme de donnée très limitée. Elles ne travaillent pas exactement sur des données indépendantes, elles font des calculs dont le résultat est utilisé par d'autres, il y a des dépendances de données entre éléments du vecteur. Cependant, il faut savoir que l'on peut émuler une instruction de réduction en utilisant des instructions verticales couplées à des instructions de manipulation de vecteur. Par exemples, prenons le cas d'une instruction de réduction qui additionne entre eux les éléments d'un vecteur. On peut l'émuler en utilisant une addition verticale entre deux vecteurs, et une instruction ''compress''. L'idée est la suivante : on coupe le vecteur initial en deux avec deux instructions ''compress'', ce qui donne deux sous-vecteurs. Puis, on additionne les deux vecteurs résultat entre eux. Puis on répète les deux étapes précédentes jusqu'à obtenir la somme finale. Le fait que l'on puisse émuler les instructions de réduction est la raison principale qui explique que les processeurs SIMD récents n’intègrent pas d'instructions de réduction. Ils se débrouillent avec des instructions SIMD de manipulation de vecteur, et des instructions SIMD verticales. C'est un bon compromis entre cout en circuits et performance. ===Les accès mémoire=== La gestion des accès mémoire est assez hétéroclite, la façon de faire étant différente selon les architectures. Dans le cas le plus simple, les données d'un vecteur sont contigües en mémoire et les instructions ont juste à préciser l'adresse mémoire du début du vecteur, qui est généralement dans un registre d'adresse spécialisé, ou un registre non-SIMD. Avec des vecteurs de 8 octets, toute instruction d'accès mémoire de ce type va lire ou écrire des blocs de 8 octets. L'adresse de départ de ces blocs est soumise à des contraintes d'alignement sur les jeux d'instructions comme le SSE, le MMX, etc. La raison à cela est que gérer des accès non alignés en mémoire rend les circuits de lecture/écriture en mémoire plus complexes. En contrepartie, ces contraintes compliquent l'utilisation des instructions SIMD par le compilateur. D'autres modes d'adressage des vecteurs permettent à une instruction de charger des données dispersées en mémoire pour les rassembler dans un vecteur. On peut notamment citer l'existence d'accès mémoires en stride et en scatter-gather. L''''accès en stride''' regroupe des données séparées par un intervalle régulier d'adresses. Ce mode d'accès a besoin de l'adresse initiale, de celle du premier élément du vecteur, et de la distance entre deux données en mémoire. Il permet aux instructions de mieux gérer les tableaux de structures, ainsi que les tableaux multi-dimensionnels. Lorsqu'on utilise de tels tableaux, il arrive assez souvent que l'on n'accède qu'à des éléments tous séparés par une même distance. Par exemple, si on fait des calculs de géométrie dans l'espace, on peut très bien ne vouloir traiter que les coordonnées sur l'axe des x, sans accès sur l'axe des y ou des z. Les instructions d'accès mémoire en enjambées gèrent de tels cas efficacement. Les processeurs SIMD incorporent aussi les accès en '''''scatter-gather'''''. De tels accès prennent en opérande un vecteur contenant des adresses mémoires, et lit/écrit chacune de ses adresses mémoire indépendamment. Les accès en ''scatter-Gather'' peuvent être vus comme une généralisation de l'adressage indirect à registre aux vecteurs, chaque élément du vecteur étant adressé via adressage indirect. Les accès en ''gather'' sont des lectures : elles prennent un vecteur d'adresse, lisent chaque adresse, et regroupent toutes les données lues dans un vecteur, dans un registre vectoriel. Les accès en ''scatter'' sont l'équivalent pour les écritures. Ils prennent un vecteur d'opérande pour les données à écrire, un vecteur pour les adresses. [[File:Cuda4.png|centre|vignette|upright=2|Adressage en scatter-gather]] Un autre version des accès en ''scatter-gather'' n'utilise pas des adresses mémoire, amis des indices. Le vecteur opérande ne contient pas des adresses, mais des indices qui sont combinés à une adresse de base pour calculer plusieurs adresses. Un tel mode d'adressage permet de faciliter l'implémentation de certaines structures de données, appelées des vecteurs de Liffe. Ils sont très utilisés pour gérer les matrices creuses, des matrices où une grande partie des éléments sont nuls et où, dans un souci d'optimisation, seuls les éléments non nuls de la matrice sont stockés en mémoire. Lors d'une instruction de ''scatter'' ou de ''gather'', tous les accès mémoire n'ont pas lieu en même temps, certains accès mémoire seront terminés avant les autres. Les accès peuvent se faire dans le désordre et de nombreuses optimisations matérielle en profitent pour gagner en performance. Il existe cependant un cas où les accès mémoire doivent se faire dans un ordre bien précis, généralement du premier élément vers le dernier : lorsqu'une instruction ''scatter'' effectue plusieurs écritures à la même adresse. C'est parfaitement possible, et c'est le seul cas où il est intéressant de forcer un ordre pour les écritures. En théorie, une instruction SIMD est un tout, c'est à dire qu'elle ne doit pas modifier les registres SIMD avant que tous les accès mémoire se soient terminés. Cependant, il est possible qu'une partie des accès mémoire déclenchent des défauts de page ou lèvent des exceptions matérielles, alors que les autres se sont terminés. En théorie, le processeur est censé se débarrasser du résultat des accès mémoire terminés. Mais ce serait un gachis de performance, ce qui fait que le processeur viole la règle voulant que les registres SIMD soient mis à jour en une seule fois à la fin de l'instruction de ''scatter-gather''. Dès qu'un accès mémoire se termine, il écrit son résultat dans le registre SIMD, à l'endroit adéquat. Cependant, ce comportement pose un problème. Lorsqu'une exception survient, comme lors d'un défaut de page, le processeur exécute la routine d'interruption associée, puis redémarre l'instruction fautive, le second essai étant le bon. Ici, le processeur ne réexecute pas l'instruction à l'identique, mais seulement les accès non-terminés. Mais comment le processeur sait-il quelles accès mémoire redémarrer ? Il doit pour cela mémoriser quels sont les accès mémoire terminés. Il utilise pour cela un registre architectural appelé le '''masque de complétion'''. Le registre est forcément architectural car il doit être préservé lors d'un changement de contexte, l’exécution de l'exception matérielle forçant un changement de contexte. ===Exemple avec les processeurs x86=== [[File:PD-20060908-SSE3-01.svg|vignette|Chronologie des extensions x86 SIMD.]] Après avoir vu la théorie sur les instructions SIMD, il est temps de voir un exemple concret : celui des instructions SIMD des processeurs x86, présents dans nos PC. Le jeu d'instruction des PC qui fonctionnent sous Windows est appelé le x86. C'est un jeu d'instructions particulièrement ancien, apparu en 1978. Depuis, des '''extensions x86''' ont ajouté des instructions au x86 de base. On peut citer par exemple les extensions MMX, SSE, SSE2, ''3dnow!'', etc. Les premières instructions SIMD furent fournies par une extension x86 du nom de MMX, introduite par Intel en 1996 sur le processeur Pentium MMX. Le MMX a perduré durant quelques années avant d'être remplacé par les extensions SSE. La même année, AMD sorti d'expansion ''3DNow!'', qui ajoutait 21 instructions SIMD similaires à celles du MMX. Celui-ci fût suivi du ''3DNow!+'' quelques années plus tard. Le SSE fût ensuite décliné en plusieurs versions avant d'être "remplacé" par l'AVX. Une grande quantité de ces extensions x86 sont des ajouts d'instructions SIMD. L’'''extension MMX''' ajoutait pas mal d'instructions SIMD assez basiques, essentiellement des instructions arithmétiques : addition, soustraction, opérations logiques, décalages, rotations, mise à zéro d'un registre, etc. La multiplication est aussi supportée, mais avec quelques petites subtilités, via l'instruction PMULLW. Les instructions MMX ne mettent pas le registre d'état à jour et ne préviennent pas en cas d'overflow ou d'underflow si ceux-ci arrivent (pour les instructions qui ne travaillent pas en arithmétique saturée). Le MMX introduisait 8 registres vectoriels, du nom de MM0, MM1, MM2, MM3, MM4, MM5, MM6 et MM7. Ils ne pouvaient contenir que des nombres entiers et faisaient 64 bits. Ils avaient cependant un léger défaut, qui a nui à l'adoption du MMX. Pour rappel, avant le MMX, les flottants étaient gérés par l'extension x87, qui définit 8 registres flottants de 80 bits. Et chaque registre MMX correspondait aux 64 bits de poids faible d'un registre flottant x87 ! Le système d'''alias'' de registres typique des CPU x86 a encore frappé ! En conséquence, il était impossible d'utiliser en même temps l'unité de calcul flottante et l'unité MMX. Par contre, sauvegarder les registres lors d'un changement de contexte, d'une interruption, ou d'un appel de fonction était très simple : la sauvegarde des registres de la FPU x87 suffisait. [[File:MMX-FPU-Register.JPG|centre|vignette|upright=2|Registres MMX et FPU x87.]] [[File:XMM registers.svg|droite|vignette|XMM registers]] Dans les années 1999, une nouvelle extension SIMD fit son apparition sur les processeurs Intel Pentium 3 : le '''Streaming SIMD Extensions''', abrévié SSE. Ce SSE fut ensuite complété, et différentes versions virent le jour : le SSE2, SSE3, SSE4, etc. Cette extension fit apparaitre 8 nouveaux registres, les registres XMM. Sur les processeurs 64 bits, ces registres sont doublés et on en trouve donc 16. En plus de ces registres, on trouve aussi un registre d'état qui permet de contrôler le comportement des instructions SSE : celui contient des bits qui permettront de dire au processeur que les instructions doivent arrondir leurs calculs d'une certaine façon, etc. Ce registre n'est autre que le registre MXCSR. Chose étrange, seuls les 16 premiers bits de ce registre ont une utilité : les concepteurs du SSE ont surement préférés laisser un peu de marge au cas où. La première version du SSE contenait assez peu d'instructions : seulement 70. Le SSE première version ne fournissait que des instructions pouvant manipuler des paquets contenant 4 nombres flottants de 32 bits (simple précision). Je ne vais pas toutes les lister, mais je peux quand-même dire qu'on trouve des instructions arithmétiques de base, avec pas mal d'opérations en plus : permutations, opérations arithmétiques complexes, autres. Petit détail : la multiplication est gérée plus simplement et l'on a pas besoin de s’embêter à faire mumuse avec plusieurs instructions différentes pour faire une simple multiplication comme avec le MMX. On peut quand même signaler une chose : des instructions permettant de contrôler le cache firent leur apparition. On retrouve ainsi des instructions qui permettent d'écrire ou de lire le contenu d'un registre XMM en mémoire sans le copier dans le cache. Ces instructions permettent ainsi de garder le cache propre en évitant de copier inutilement des données dedans. On peut citer par exemple, les instructions MOVNTQ et MOVNTPS du SSE premiére version. On trouve aussi des instructions permettant de charger le contenu d'une portion de mémoire dans le cache, ce qui permet de contrôler son contenu. De telles instructions de prefetch permettent ainsi de charger à l'avance une donnée dont on aura besoin, permettant de supprimer pas mal de cache miss. Le SSE fournissait notamment les instructions PREFETCH0, PREFETCH1, PREFETCH2 et PREFETCHNTA. Autant vous dire qu'utiliser ces instructions peut donner lieu à de sacrés gains si on s'y prend correctement ! Il faut tout de même noter que le SSE n'est pas seul "jeu d'instruction" incorporant des instructions de contrôle du cache : certains jeux d'instruction POWER PC (je pense à l'Altivec) ont aussi cette particularité. Avec le '''SSE2''', de nouvelles instructions furent ajoutés, permettant d'utiliser des nombres de 64, 16 et 8 bits dans chaque vecteur. Le SSE2 incorporait ainsi pas moins de 144 instructions différentes. Ce qui commençait à faire beaucoup. Puis, vient le '''SSE3''', avec ses 13 instructions supplémentaires. Pas grand-chose à signaler, si ce n'est que des instructions permettant d'additionner ou de soustraire tous les éléments d'un paquet SSE ensemble, des instructions pour les nombres complexes, et plus intéressant : les deux instructions MWAIT et MONITOR qui permettent de paralléliser plus facilement des programmes. Le '''SSE4''' fut un peu plus complexe et fut décliné lui-même en 2 versions. Le SSE4.1 introduit ainsi des opérations de calcul de moyenne, de copie conditionnelle de registre (un registre est copié dans un autre si le résultat d'une opération de comparaison précédente est vrai), de calcul de produits scalaire, de calcul du minimum ou du maximum de deux entiers, des calculs d'arrondis, et quelques autres. Avec le SSE4.2, le vice à été poussé jusqu'à incorporer des instructions de traitement de chaines de caractères. [[File:AVX registers.svg|vignette|Registres AVX.]] Avec l''''AVX (Advanced Vector eXtensions)''', on retrouve 16 registres d'une taille de 256 bits, nommés de YMM0 à YMM15 et dédiés aux instructions AVX. Ils sont partagés avec les registres XMM : les 128 bits de poids faible des registres YMM ne sont autres que les registres XMM. L'AVX complète le SSE et ses extensions, en rajoutant quelques instructions, et surtout en permettant de traiter des données de 256 bits. Son principal atout face au SSE est que les instructions AVX permettent de préciser le registre de destination en plus des registres d'opérandes. Avec le SSE et le MMX, le résultat d'une instruction SIMD était écrit dans un des deux registres d'opérande manipulé par l'instruction. Il fallait sauvegarder son contenu si on en avait besoin plus tard, ce qui n'est plus nécessaire avec l'AVX. ==La performance des processeurs SIMD== Il est intéressant de comparer la performance d'un processeur SIMD et celle d'un processeur normal, sans SIMD. Mais cet exercice est compliqué par le fait que les processeurs non-SIMD sont très nombreux : entre les CPU superscalaires, à émission dans l'ordre, exécution dans le désordre, ceux sans rien de tout cela, on a de quoi être perdu. Dans ce qui suit, nous allons comparer deux processeurs basiques sans pipeline : un avec SIMD, un autre sans. Comparé à un processeur sans pipeline, on s'attend à ce que la performance soit augmentée d'un facteur N, avec N le nombre maximal d'entiers/flottants que l'on peut mettre dans un vecteur. Si une architecture SIMD fait N calculs en parallèles, alors les performances sont censées être multipliées par N. Cela laisse penser que plus les vecteurs sont longs, plus le gain en performance est important. Il s'agit là d'un résultat basique, mais la vraie vie est différente. Un premier problème est que ce résultat ne tient que si la mémoire RAM et les caches suivent. En effet, faire N calculs en parallèle demande de lire/écrire N fois plus de données. Les données sont souvent stockées dans des registres vectoriels, ce qui fait que la pression sur le banc de registre est assez importante. Mais cela signifie aussi que les instructions d'accès mémoire doivent lire/écrire N fois plus de données. Il faut alors ajouter des ports sur le cache, élargir le bus mémoire, augmenter la taille des lignes de cache, etc. Le débit binaire des caches et de la mémoire RAM deviennent rapidement des points limitants, qui réduisent le gain en performance des instructions SIMD. Et ne parlons pas de l'interaction avec la mémoire virtuelle : vu qu'on lit/écrit par paquets de données, cela signifie qu'on traverse une page mémoire plus vite, ce qui fait que les défauts de page sont lus fréquents. Un processeur SIMD a intérêt à être combiné à une RAM et des caches solides, très performants. C'est le cas sur les processeurs modernes, mais cela a pose des problèmes pour les processeurs vectoriels, et a mené à leur abandon progressif. Mais passons outre ce problème et regardons la seconde raison qui font que ce résultat est naïf : la loi d'Amdhal ! Toutes les portions d'un programme ne sont pas accélérées par des instructions SIMD, il y a des portions dont les calculs ont des dépendances de données, d'autres qui ne sont pas parallélisables, etc. Une partie du programme est donc impossible à véctoriser (i.e optimiser pour le SIMD), une l'autre l'est. Pour simplifier les explications, on suppose qu'une instruction SIMD prend le même nombre de cycles que son équivalent non-SIMD. Par exemple, une addition SIMD prend le même temps qu'une addition normale. Dans ce cas, la portion non-vectorisable du code reste inchangée, alors que la portion vectorisable est divisée par N, par la largeur du vecteur. On retombe sur une formulation identique à la loi d'Amdhal sur le fond. Une manière de quantifier le tout est d'utiliser la métrique de l'efficience SIMD. Elle est calculée en divisant deux grandeurs. La première est elle-même un rapport : c'est le gain obtenu en terme de nombre d'instructions exécutées. Prenons un programme qui exécute I instructions avant vectorisation, et qui en exécute I/X après. Le gain s'exprime comme suit : : <math>G = \frac{I}{I/X} = X</math> Maintenant, prenons ce gain est divisons-le par la largeur d'un vecteur. On obtient alors l'efficience SIMD : : <math>\text{Efficience SIMD} = \frac{G}{N} = \frac{\frac{I}{I/X}}{N}</math> Notons que le gain et la largeur SIMD ne sont égales que dans un cas bien précis : tout le code est vectorisable. Il s'agit donc d'une reformulation du gain de la loi d'Amdhal, mais dans laquelle le pourcentage de code série est caché. Il est intéressant de regarder l'efficience SIMD quand on augmente la portion du code série, ainsi que la taille des vecteurs. Prenons un cas assez généreux, réaliste pour certaines applications très adaptées au SIMD : 1% du code est non-vectorisable, 99% profite du SIMD. L'efficience SIMD varie alors assez rapidement avec la taille des vecteurs. De 99% pour N=2, elle descend à 98% pour N=16, et tombe sous les 50% pou N=128. La conséquence est que les très grandes tailles de vecteurs ne sont pas vraiment utiles, le rendement est décroissant avec la taille des vecteurs. ==Les processeurs SIMD à registres généraux== Le premier type de processeur SIMD que nous allons voir est celui des processeurs de type '''SWAR''' (''SIMD Within A Register''). Le terme SWAR est un terme polysémique dont le sens a beaucoup changé au fil du temps. Ici, nous allons l'utiliser dans la définition la plus stricte : celle où on effectue du SIMD dans les registres généraux du processeur, sans registres spécialisés. Il s'agit d'une forme de SIMD qui est aujourd'hui peu utilisée, mais qui a été la première forme de SIMD utilisée dans des processeurs grand public commerciaux. L'idée est d'améliorer un petit peu un processeur normal, non-SIMD. Un processeur usuel contient des registres généraux de 32 à 64 bits, qui stockent chacun un opérande de même taille que le registre. D'anciens processeurs avaient des instructions pour effectuer des calculs simples, sur des opérandes plus courts. Par exemple, les processeurs x86 32 bits sont capables de faire des calculs sur 8, 16, ou 32 bits. Mais on ne pouvait placer qu'un seul opérande de 8, 16 ou 32 bits dans un registre général, ce qui est un léger gâchis. Le SWAR est une amélioration de cette technique qui vise à mieux utiliser les registres et l'ALU. L'idée est de regrouper plusieurs opérandes de 8, 16, voire 32 bits, dans un seul registre général. De plus, on ajoute des instructions SIMD d'addition/soustraction/autres, qui lisent des opérandes 8/16/32 bits dans ces registres généraux. Par exemple, sur un processeur 32 bits, on peut ajouter une opération d'addition qui lit deux registres de 32 bits, pour récupérer quatre opérandes de 16 bits (deux par registre) et effectue deux additions 16 bits en même temps dans l'ALU. Et il y a la même chose pour d'autres opérations, comme la soustraction, la comparaison, etc. Les instructions SIMD disponibles sur ces processeurs se limitent généralement à des additions, soustractions, comparaisons, mais guère plus. De plus, il s'agit d'instructions entières, il n'y a pas d'instructions SIMD flottantes. La raison à cela est très simple, mais nous l'expliquerons plus bas, dans la section sur l'implémentation. Disons simplement que de tells architectures visent une économie en circuit maximale. Elles implémentent les instructions SIMD en utilisant le moins de circuits possibles, ce qui fait qu'elles réutilisent les registres généraux et n'ont pas de registres SIMD séparés. ===Les extensions/jeux d'instructions de type SWAR=== Les premiers processeurs à intégrer du SWAR étaient les processeurs DEC Alpha, avec l'extension multimédia '''''Motion Video Extensions'''''. Les processeurs en question étaient les processeurs Alpha 21164PC (PCA56 and PCA57), Alpha 21264 (EV6) and Alpha 21364 (EV7). Les instructions SIMD disponibles étaient très simples et se résumaient à quelques comparaisons : trouver le maximum ou le minimum de deux opérandes de 8/16 bits, quelques instructions de permutation. Les extensions '''''Multimedia Acceleration eXtensions''''' des processeurs Hewlett-Packard PA-RISC sont aussi de ce type, mais disposent de plus d'instructions SIMD. La première version, appelée MAX-1, était disponible sur les processeurs 32 bits de la marque, à partir du processeur PA-7100LC. Les instructions disponibles étaient des additions et soustractions d'opérandes 16 bit. Il y avait en tout trois instructions d'addition et trois pour la soustraction. En tout, il y avait : * une instruction d'addition/soustraction non-signée utilisant l'arithmétique modulaire ; * une instruction d'addition/soustraction signée en arithmétique modulaire ; * une instruction d'addition/soustraction signée en arithmétique saturée. Outre les additions/soustractions, il y avait aussi une instruction pour calculer la moyenne de deux opérandes 16 bits, ainsi qu'une addition fusionnée avec un décalage. La seconde version, appelée MAX-2, ajouta des instructions d'additions fusionnées avec des décalages, mais aussi des instructions de permutation. De plus, cette version était disponible sur les processeurs 64 bits de la marque, ce qui fait que les registres étaient eux aussi de 64 bits. Ils pouvaient contenir deux fois plus d'opérandes 16 bits. Il n'y avait de gestion d'opérandes 32 bits pour les extensions SIMD. ===L'implémentation matérielle=== L'avantage de cette technique est la grande simplicité d'implémentation. On n'ajoute pas de registres SIMD séparés, ce qui a de nombreux avantages : économie de circuits car pas besoin d'un second banc de registre, pas besoin de sauvegarder des registres SIMD en plus lors d'un appel système/interruption, etc. L'implémentation a juste besoin de modifier le décodeur et l'unité de calcul. Le décodeur est modifié pour ajouter des instructions en plus, rien de spécifique au SWAR. Les modifications de l'unité de calcul sont spécifiques au SWAR et modifient la manière dont elle gère les retenues. Une première solution est d'utiliser une ALU bit-slicée, ce qui est l'idéal pour les unités de calcul entières. Par exemple, pour une ALU 32 bits entière, on peut la découper en 4 unités de calcul 8 bits. Pour une opération SIMD avec 4 opérandes 8 bits, les quatre ALU fonctionnent en parallèle, il n'y a pas transmission des retenues. Pour les calculs SIMD avec des opérandes de 16 bits, on regroupe les ALU par paire et on propage les retenues à l'intérieur d'une paire, mais pas entre les paires. Enfin, pour un calcul non-SIMD, les opérandes de 32 bits, on propage les retenues normalement, d'une ALU 8 bits vers la suivante. Une autre solution prend un additionneur 32/64 bits normal, mais mets à 0 les retenues. L'implémentation est très simple avec un additionneur à anticipation de retenues, où les retenues sont calculées en avance, avant de faire l'addition proprement dite, par un circuit d'anticipation de retenue. Il suffit alors d'ajouter un circuit de masquage en sortie du circuit d'anticipation de retenue, qui met à zéro les retenues adéquates. La gestion des débordements demande d'ajouter des circuits, ou du moins de modifier ceux déjà présents dans l'ALU. Typiquement, les circuits pour gérer l'arithmétique saturée sont un peu modifiés pour effectuer la mise à 1111...111 octet par octet et chaque octet est masqué si besoin. [[File:ALU modifiée pour implémenter du SWAR.png|centre|vignette|upright=3|ALU modifiée pour implémenter du SWAR]] Notons que la technique ne s'applique qu'aux additions, et aux opérations dérivées comme la soustraction et les comparaisons (ces dernières sont des soustractions déguisées). Mais les multiplications ou divisions ne peuvent pas s'implémenter simplement en modifiant des ALU existantes, du moins pas avec des modifications simples. Aussi, les processeurs qui utilisent cette technique se bornent aux additions et opérations dérivées, pas plus. La gestion des opérations de permutation est quant à elle très simple : beaucoup de processeurs disposent déjà d'instructions de permutation d'octets, qui sont l'équivalent d'instructions SIMD de permutation pour les registres généraux. Nous en avions déjà parlé dans le chapitre sur le langage machine et l'assembleur, quand nous avions fait la liste des instructions les plus courantes. De telles instructions de permutation d'octet sont utiles pour gérer le boutisme ou effectuer quelques manipulations assez rares. Pas besoin de rajouter une ALU dédiée, celle-ci est déjà présente de base, du moins si les instructions de permutation d'octet sont déjà présentes. ==Les processeurs SIMD purs (''packed SIMD'')== Maintenant que nous avons vu les instructions SIMD, passons maintenant aux processeurs eux-même. Tous les processeurs que nous allons voir dans ce qui suit supportent des instructions SIMD. Mais leur implémentation matérielle, leur micro-architecture, n'est pas la même. Ils ont aussi quelques différences en termes de jeu d'instruction, mais qui sont fortement liées à l'implémentation matérielle. Nous allons ici voir les '''processeurs SIMD purs''', aussi dits de type ''Packed SIMD''. Ils implémentent des instructions SIMD sans rien de plus, juste le strict minimum : pas de prédication, pas de vecteurs de taille variable, presque pas d’instructions horizontales, architecture de type LOAD-STORE. De tels processeurs utilisent plusieurs ALU entières/flottantes qui travaillent en parallèle. De plus, ils disposent de registres SIMD séparés des registres généraux. Voyons ces points immédiatement. * La première distinction est que les vecteurs sont stockés dans des registres séparés des registres généraux, appelés les '''registres SIMD''', ou ''registres vectoriels''. Ils sont beaucoup plus larges que les registres classiques et mémorisent 2, 4, 8 entiers/flottants. * La seconde distinction est que les calculs SIMD sont effectués dans une unité SIMD séparée de l'ALU ou de la FPU. L'unité de calcul SIMD peut exécuter au minimum des instructions SIMD verticales, plus rarement des instructions SIMD horizontales. Pour ce faire, elle regroupe plusieurs additionneurs/multiplieurs, qui effectuent des calculs en parallèle. ===Les instructions SIMD verticales : plusieurs ALU travaillant en parallèle=== [[File:SIMD2.svg|vignette|Parallélisme de données au niveau de l'unité de calcul. Celle-ci contient plusieurs circuits indépendants qui appliquent la même opération sur des données différentes.]] Une unité de calcul SIMD gère au minimum des instructions SIMD verticales. Et pour ce faire, elle contient plusieurs additionneurs/multiplieurs séparés. Par exemple, pour additionner deux vecteurs contenant chacun 16 entiers/flottants, il faut utilise 16 additionneurs entiers et 16 additionneurs flottants. Dans le cas général, une ALU SIMD est composée de plusieurs ALU entières et flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. Notons que les ALU travaillent en parallèle, elles font des calculs indépendants. Notons que les additionneurs dans chaque ALU doivent pouvoir être configurés de manière à gérer des tailles de données différentes. Par exemple, si on prend un vecteur simple, qui peut contenir soit 32 entiers de 16 bits, soit 16 entiers 32 bits, soit 8 entiers 64 bits, alors on doit utiliser 8 additionneurs, mais chacun d'entre eux doit pouvoir être reconfiguré de manière à ne pas propager les retenues au-delà des premiers 16 ou 32 bits. Un défaut de cette organisation est que le cout en circuits est loin d'être négligeable. Il faut dupliquer des unités de calcul et les coller en rajoutant des circuits, cela utilise beaucoup de transistors. Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures. Retenez cependant : l'usage de plusieurs ALU travaillant en parallèle a plusieurs défauts. Premièrement, cela implique des vecteurs de taille fixe, du fait que le nombre d'ALU travaillant en parallèle est fixe. Deuxièmement, des difficultés d'implémentation concernant les instructions de réduction. Détaillons ce second point ! Une autre possibilité est d'utiliser moins d'unités de calcul qu'il n'y a d'éléments dans un vecteur. Par exemple, pour des vecteurs contenant 32 flottants, il se peut qu'il n'y ait que 16 ALU. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale. L'avantage est que l'ensemble des calculs se fait en une seule instruction machine. L'implémentation est généralement la suivante : le processeur décode une instruction SIMD en deux micro-instructions SIMD plus courtes. Par exemple, pour une instruction SIMD de 32 éléments, elle sera décodée en deux instructions SIMD de 16 éléments, chacune étant exécutée sur l'ALU. Pas mal de processeurs x86 font cela pour les instructions AVX 512, qui travaillent sur des vecteurs de 512 bits. Les premières version de l'AVX travaillaient sur des vecteurs de 256 bits, l'AVX-512 étendit le tout à 256 bits. Lors de l'introduction de l'AVX, la plupart des CPU x86 avaient une ALU 256 bits et les concepteurs de processeurs ont préféré la garder telle quelle. Les ALU SIMD travaillent donc sur des vecteurs de 256 bits, les instructions 256 bits étaient traitées comme une seule µops et les instructions 512 bits avec deux µops de 256 bits. Il est possible de réduire fortement l'impact en performance en doublant la fréquence de l'ALU. Par exemple, pour des vecteurs contenant 32 flottants, le processeur incorpore 16 ALU, mais celles-ci fonctionne à une fréquence double de celle du processeur. L'usage d'une ALU à double fréquence est une technique qui a été utilisée sur le Pentium 4 et quelques processeurs, pour des instructions non-SIMD. Mais pour les processeurs SIMD, elle s'applique à la perfection. L'implémentation est la même que précédemment, sauf que le ''scheduler'' fonctionne à double fréquence. Pour limiter la casse, il est préférable d'utiliser un ''scheduler'' séparé pour les instructions SIMD, qui sera seule à fonctionner à double fréquence. ===Les instructions horizontales : une ALU simple séparée=== Les instructions horizontales posent des difficultés d’implémentation. Elles ne peuvent pas s'implémenter en utilisant plusieurs ALU travaillant en parallèle, contrairement aux instructions verticales, ce qui fait qu'elles utilisent généralement des unités de calcul spécialisées. Et c'est là que la différence entre instructions de manipulation de vecteurs et instructions de réduction vient encore une fois poser problème. Les instructions de manipulation de vecteur sont relativement simples à implémenter : elles ont juste besoin d'une ALU spécialisée, relativement simple, composée de beaucoup de multiplexeurs à configurer convenablement. Mais pour les instructions de réduction, c'est autre chose ! L'implémentation des instructions de réduction est possible mais a un cout en circuits assez prohibitif. Pour additionner N entiers, il faut utiliser un additionneur multi-opérande qui prend beaucoup de circuits et dont le temps de calcul est proche de celui d'une multiplication (très lent). Trouver le maximum et le minimum d'un nombre demandent de faire la même chose avec des comparateurs. Le tout a un cout en circuit non-négligeable, pour accélérer des opérations assez peu fréquentes. Elles sont surtout utilisées pour trouver la somme ou le maximum/minimum d'un tableau, opération séquentielle par nature, avec peu parallélisme de données, peu courante. Une autre solution utilise une unité SIMD normale, mais intègre un système de contournement, de ''bypass'', qui relierait l'entrée d'une ALU à la sortie d'une autre. Mais le cout lié aux interconnexions est très important : on doit relier chaque ALU à toutes les autres dans le pire des cas, on peut optimiser le tout et réduire un peu la quantité d'interconnexions, mais le cout reste important. Au final, on gagne en circuits ce qu'on perd en interconnexions. Le choix entre les deux solutions est loin d'être facile et dépend du processeur, de la technologie utilisée, du budget en transistors disponibles, etc. Les processeurs SIMD purs résolvent ce problème assez simplement : ils n'implémentent pas d'instructions de réduction. Par contre, ils implémentent systématiquement des instructions de manipulation de vecteur, comme des permutations ou autres. La raison est que l'on peut émuler les instructions de réduction en utilisant des instructions SIMD de manipulation de vecteur couplées à des additions/multiplications SIMD verticales. Il s'agit donc d'un compromis entre cout en circuits et performance finale. Niveau circuits, on a une unité SIMD avec plusieurs ALU de calcul en parallèle, une unité de manipulation de vecteur séparée assez simple et au cout en circuits raisonnable. Les performances pour les opérations de réduction sont acceptables, le cout en performance est relativement modéré. ===Le banc de registre vectoriel=== La présence de registres vectoriels séparés permet d'avoir des vecteurs assez longs. Pour ce faire, les registres vectoriels sont plus grands que les registres généraux. Par exemple, sur un processeur 32 bits, qui gère donc des entiers de 32 bits, les vecteurs peuvent faire 128 ou 256 bits. Un vecteur contient donc plusieurs entiers ou flottants de taille maximale. On n'est pas dans le cas précédent, où registres généraux et SIMD sont les mêmes, ils sont séparés et n'ont pas la même taille. Le processeur utilise un banc de registre séparé pour les registres SIMD, appelé le '''banc de registre SIMD'''. Vu que les registres vectoriels sont assez longs, le banc de registre vectoriels a une grande taille, une grande capacité mémoire. Pour donner un exemple, les extensions SSE des processeurs x86 définissent 8 registres SIMD de 128 bits chacun. Ils ont été introduits à une époque où les processeurs avaient des registres généraux de 32 bits. Le banc de registres associé était donc environ 4 fois plus grand que celui pour les registres généraux. Je dis environ car le décodeur avait sensiblement la même taille, seul le plan mémoire était grossit, mais l'idée est là. Avec les extensions x86 suivant le SSE, les registres ont été étendus à 256 bits avec l'AV, 512 avec l'AVX-512. Comme dit plus haut, l'AVX-512 supporte 32 registres AVX de 512 bits, les 16 registres 256 bits de l'AVX normal étant inclus dedans. Intuitivement, on peut penser que le processeur contient 32 registres de 512 bits, avec un système pour sélectionner seulement 256 bits. Mais la réalité peut être différente. Sur les premiers processeurs supportant d'AVX 512, le banc de registre était est composé de deux plans mémoire séparés, chacun contenant des registres de 256 bits. Le résultat est que si le processeur n'utilise pas l'AVX-512, mais seulement les registres 256 bits, un banc de registre n'est pas utilisé, il est mis en veille pour économiser de l'énergie. PAr la suite, les CPU x86 sont passé à un plan mémoire unique. Il faut préciser que les registres SIMD sont eux aussi renommés, comme tout registres. Et ce renommage doit faire attention à la taille des registres renommés, notamment quand les registres sont étendus en passant de 128 à 256 bits, ou de 256 bits à 512 bits. Mais cela peut ouvrir des optimisations intéressantes. Pour donner un exemple, prenons la microarchitecture Golden Cove d'Intel. Avec le renommage de registre, Golden Cove avait en tout 327 registres 256 bits, mais seulement 242 pour les registres 512 bits. Vous avez bien lu : il y a moins de registres 512 bits que de registres 256 bits ! Mais le renommage de registre fait passer cela sous silence. Une conséquence est que le banc de registres SIMD consomme beaucoup d'énergie et d'électricité. Il en est de même avec les ALU SIMD. Et diverses optimisations tentent de réduire ce cout en énergie. L'une d'entre ajoute une mémoire cache entre les ALU SIMD et les registres vectoriels ! L'idée est que si un registre vectoriel est relu plusieurs fois de suite, la première lecture lira le vecteur dans le banc de registres SIMD, mais les lectures suivantes liront le vecteur dans le cache. L'optimisation marche car accéder au banc de registre vectoriel est vraiment couteux, plus qu'accéder à un cache. Le cache en question est appelé un '''''register reuse caches'''''. Les ''register reuse caches'' sont présents sur le processeur Monaka de Fujitsu, mais aussi et surtout dans les cartes graphiques modernes. Nous ferrons l'impasse sur les GPU modernes, car leur usage est quelque peu particulier. Il ne servent pas que à réduire la consommation d'énergie, mais aussi à réduire les conflits d'accès au banc de registre, qui est en réalité composé de plusieurs banques indépendantes. Leur usage sur le CPU Monaka est plus simple : ils ne servent vraiment qu'à réduire la consommation d'énergie, rien de plus. : Pour ceux qui veulent en savoir plus pour leur utilisation sur les cartes graphiques, tout est détaillé dans mon wikilivre sur les cartes graphiques, via le lien suivant. Mais sachez cependant qu'il faut idéalement lire le chapitre entier, et peut-être même les chapitres précédents, pour comprendre cette section. Voici le lien : [https://fr.wikibooks.org/wiki/Les_cartes_graphiques/La_microarchitecture_des_processeurs_de_shaders#L'Operand_Collector_et_les_caches_de_register_reuse L'Operand_Collector_et_les_caches_de_register_reuse] ===L'implémentation des LOAD-STORE à des données consécutives=== Les architectures SIMD pures sont des architectures de type LOAD-STORE, ce qui veut dire que les instructions SIMD ne peuvent que lire ou écrire dans les registres vectoriels, pas en mémoire ou ailleurs. Les transferts entre registres SIMD et mémoire sont le fait d'instructions LOAD et STORE spécialisées, qui transfèrent des vecteurs entre RAM et registres vectoriels. Les autres architectures n'ont pas nécessairement ce genre de restrictions, mais laissons cela pour plus tard. Implémenter les instructions LOAD/STORE pour des vecteurs dont les données sont consécutives en mémoire n'est pas très compliqué. Il suffit juste d'élargir le port de lecture/écriture du cache L1 de données, pour pouvoir lire/écrire un vecteur entier. Rien de bien compliqué, il suffit juste d'ajouter des interconnexions et d'ajouter des multiplexeurs. Du moins, c'est le cas tant que les vecteurs sont plus petits qu'une ligne de cache, ce qui est souvent le cas en pratique. Dans le cas très rare où les vecteurs sont plus longs qu'une ligne de cache, les LOAD/STORE doivent se faire en deux accès dans le cache, ce qui demande d'ajouter du matériel pour contrôler la situation. Précisons cependant qu'il s'agit là d'un cas idéal, où les vecteurs sont correctement alignés en mémoire. En effet, il se peut qu'un vecteur soit à cheval sur deux lignes de cache, alors qu'il est plus petit qu'une ligne de cache. Il suffit pour cela qu'il soit placé à une adresse adéquate. Par exemple, prenons un vecteur de 16 octets et des lignes de cache de 32 octets. Si le vecteur démarre à l'adresse 24, alors il sera à cheval sur deux lignes de cache. Il faut donc intégrer des circuits pour gérer cette situation. Pour cela, il suffit d'ajouter un registre pour stocker la première de ligne lue, puis un circuit qui combine les deux lignes de cache pour donner le vecteur final. ===L'implémentation des LOAD-STORE en ''scatter-gather''=== L'implémentation des accès en ''scatter-gather'' est plus compliquée. Un accès en ''scatter-gather'' demande de faire des calculs d'adresse, suivis par un ensemble de lectures/écritures, suivis par un regroupement des données lues dans un vecteur. Le calcul d'adresse peut se faire en parallèle dans une unité de calcul SIMD, dans plusieurs unités de calcul d'adresse en parallèle. Par contre, les lectures/écritures ne le peuvent pas. Le cache n'a pas assez de ports de lecture/écriture pour lire/écrire N données. Il a quelques ports, ce qui lui permet de lire/écrire 3/4 données grand maximum, soit moins que ce qui est nécessaire pour faire un accès en ''scatter-gather'' en une fois. Les lectures/écritures sont donc effectuées en plusieurs fois. Pour une opération de ''Gather'', il faut aussi regrouper les données lues dans un vecteur. Sur les caches des processeurs à haute performance, le calcul d'adresse est intégré dans le décodeur. Ce sont des caches adressés par somme, que nous avions abordé dans le chapitre sur les mémoires caches. Avec de telles caches, on ne peut pas utiliser une unité de calcul d'adresse SIMD, on ne peut calculer qu'autant d'adresse qu'il y a de ports sur le cache. Les accès en ''scatter-gather'' regroupent plusieurs accès mémoire, qui peuvent être effectués dans le désordre. La seule exception est celle où un ''scatter'' effectue plusieurs écritures à la même adresse, où les deux écritures doivent se faire dans l'ordre allant du premier au dernier élément du vecteur d'adresse. Les processeurs SIMD profitent de cette absence d'ordre pour effectuer les accès dans un ordre le plus optimisé possible. Par exemple, imaginons le cas où une instruction ''gather'' lise des éléments placés dans deux lignes de cache uniquement, mais dans le désordre en passant sans cesse d'une ligne de cache à l'autre. Il est alors possible de faire tous les accès dans la première ligne de cache en premier, avant de faire ceux allant dans l'autre. Une telle optimisation marche aussi bien pour les lectures que les écritures, pour les ''gather'' que pour les ''scatter''. Elle peut aussi être adaptée pour plus de deux lignes de cache. Elle porte le nom de '''''memory coalescing'''''. Une autre optimisation possible est de détecter le cas où un accès en ''scatter-gather'' accède à des données consécutives. Il suffit pour cela que les indices/adresses du vecteur opérande soient consécutives, et cela arrive plus souvent qu'on ne le pense. Dans ce cas, l'accès est remplacé par une instruction LOAD/STORE SIMD normale, beaucoup plus simple et plus rapide. Les deux optimisations demandent que les adresses soient calculées avant de faire l'accès mémoire. Les adresses calculées sont alors comparées entre elles, par un réseau de comparateurs assez complexe. Un point important de l'implémentation des instructions ''gather'' est qu'une donnée chargée doit être insérée au bon endroit dans le vecteur destination. Et pareil pour les ''scatter'' : il faut lire la bonne donnée au bon moment. Si on prend une ligne de cache complète, il faut lire plusieurs élèments en même temps et les envoyer au registre de destination au bon endroit. Il faut pour cela tout un réseau de multiplexeurs assez complexe pour faire ce travail. Il y a le même genre de réseau pour les instructions ''scatter'', mais qui va dans l'autre sens : du registre vers la ligne de cache. ==Les processeurs SIMD avec prédication== Un obstacle très gênant à la vectorisation est la présence de branchements conditionnels dans les boucles à vectoriser. Si une boucle contient des branchements conditionnels, elle ne peut pas être vectorisée facilement : il est impossible de zapper certains éléments d'un vecteur suivant une condition. Il n'est pas possible d'effectuer une instruction SIMD seulement sur certains éléments d'un vecteur et d'ignorer les autres, en fonction du résultat d'un branchement conditionnel. Tout le vecteur y passe, ou le vecteur est épargné, mais la condition intermédiaire n'est pas possible avec du SIMD simple. Pour résoudre ce problème, les processeurs SIMD incorporent diverses techniques pour implémenter ou éviter les branchements conditionnels le plus possible. La plus simple est l'incorporation de certaines instructions SIMD simples, comme la valeur absolue, le calcul du minimum/maximum, etc. Ces opérations sont généralement émulées en utilisant des branchements, avec quelques conditions simples. Incorporer ces instructions permet ainsi de faire disparaitre une partie des branchements du code. Elle ne paye pas de mine, mais elle a un résultat pas négligeable pour le rendu graphique, certaines applications de calcul scientifiques, et quelques autres. Son cout en transistors est très variable, il faut rajouter des circuits dans le séquenceur, l'unité de calcul SIMD est assez peu modifiée (il faut juste rajouter des multiplexeurs). Le vrai problème est que cette solution laisse énormément de branchements dans le code. Et qu'il faut donc trouver une solution plus générale, capable de réduire drastiquement les branchements. Pour cela, on réutilise une technique vue dans les chapitre sur l'assembleur et le langage machine : la prédication. ===Les instructions à prédicats=== Pour optimiser le cas général, il est possible d'utiliser des instructions à prédicats adaptées pour fonctionner sur des vecteurs. L'idée est que quand une instruction effectue un calcul sur un ou deux vecteurs, certains éléments du vecteurs sont ignorés. Les éléments à ignorer sont choisis suivant le résultat d'une instruction de comparaison, qui effectue un test : les éléments pour lesquels ce test est respecté sont pris en compte, ceux qui ne passent pas le test sont ignorés. Pour donner un exemple d'utilisation, imaginons que l'on ait un vecteur dans lequel on veut remplacer toutes les valeurs négatives par des 0. Dans ce cas, on utilise : * une instruction de comparaison, qui compare chaque élément du vecteur avec 0 et génère plusieurs bits de résultat ; * suivi d'une instruction à prédicat qui met à zéro les éléments pour lesquels les bits de résultat précédents sont à 1. La prédication peut aussi être utilisée pour simuler des vecteurs de taille variable. Tous les vecteurs ont une taille fixe, mais on peut ne pas faire de calculs sur la fin d'un vecteur, ce qui fait que tout se passe comme si le vecteur était plus petit. Ce n'est pas sa seule utilisation, mais la prédication permet de simuler ce comportement. Néanmoins, cela n'en fait pas un processeur vectoriel, capable de traiter des vecteurs de taille variable : il n'y a pas de moyen de préciser explicitement la taille des vecteurs à traiter. Tout cela demande plusieurs modifications du jeu d'instruction : ajouter des instructions de comparaison SIMD, ajouter de quoi faire la prédication. ===Le calcul des masques et le registre de masque=== La prédication demande que le résultat d'une comparaison soit stocké dans un registre à prédicat ou dans le registre d'état. Pour les processeurs SIMD, il y a cependant une grosse adaptation à faire concernant le fonctionnement des comparaisons et le stockage des résultats. Premièrement, les instructions de comparaison SIMD comparent une paire de deux éléments à la même place dans deux vecteurs et fournissent un résultat d'un bit pour chaque paire. Le résultat est donc un ensemble de bits, qui sont regroupées dans un vecteur spécialisé. Deuxièmement, reste à savoir que faire de ce vecteur. Pour cela, deux solutions sont possibles. Avec la première, le vecteur est stocké dans un registre vectoriel/SIMD comme les autres, il est traité comme n'importe quel autre vecteur. Avec la seconde solution, il reçoit un registre dédié, qui est un mix entre registre d'état et registre à prédicat, appelé un '''registre de masque''' (''Vector Mask Register''). Il stocke un bit pour chaque donnée présente dans le vecteur à traiter, qui indique s'il faut ignorer la donnée ou non. L'instruction SIMD suivante fait usage de ce masque pour savoir s'il faut faire l'opération pour chaque élément du vecteur. [[File:Vector mask register.png|centre|vignette|upright=2|Vector mask register]] Il peut y avoir un ou plusieurs registres de masque. S'il y en a plusieurs, ils sont généralement nommés, sur le modèle des registres à prédicats. L'application d'un masque demande de fournir le nom du registre de masque à utiliser, ils adressés explicitement dans les instructions. S'il n'y en a qu'un seul, il est possible de le pas le nommer et de faire en sorte qu'il soit adressé implicitement par les instructions qui en ont besoin. On parle alors de '''masquage implicite'''. Le masquage implicite est très utilisé sur les cartes graphiques, qui sont actuellement des architectures SIMD, comme nous le verrons plus bas. Un gros problème du masquage implicite est qu'il introduit des dépendances implicites entre instructions, qui ne sont pas explicites quand on regarder uniquement les registres manipulés par une instruction, qui perturbent le renommage de registres. Rien d'insurmontable, mais cela peut poser des problèmes de performance et la résolution de ce problème a un cout en hardware. Mais ce n'est un problème que sur les architectures à exécution dans le désordre. Les architectures à émission dans l'ordre ne sont pas concernées, et c'est notamment le cas sur les GPU et les cartes graphiques modernes. Diverses optimisations sont possible avec un registre de masque. La première est de ne pas exécuter d'instruction si tous les bits du masque sont à 0. Dans ce cas, l'instruction SIMD n'a pas de calculs à faire, vu que tous les résultats sont masqués. Il est alors possible de ne pas exécuter l'instruction SIMD et de passer directement à la suivante. L'implémentation matérielle est très simple : une porte NOR qui détermine si le registre de masque contient la valeur 0. La sortie de cette porte NOR est envoyée au séquenceur d'instruction. Le décodeur est alors conçu pour zapper l'instruction si le registre de masque contient un zéro. ===Les accès mémoire en ''scatter-gather'' avec prédication=== Un problème avec les accès mémoire que certains accès peuvent déclencher des défauts de page ou d'autres formes d'exceptions (erreurs de protection mémoire, autre). Si l'accès mémoire se fait sans prédication, ces exceptions sont gérées dans l'ordre des accès mémoire, en commençant par le début du vecteur, ce qui rend l'implémentation assez facile. Mais avec la prédiction, il se peut qu'un accès masqué et censé être ignoré déclenche une exception matérielle. Et le résultat dépend de l'implémentation. Une première solution est d'effectuer l'accès mémoire, et de masquer ensuite les éléments à ignorer. Mais dans ce cas, les éléments masqués peuvent déclencher des exceptions, qui ne sont pas censées se produire. Une autre solution est de déterminer quelles sont les adresses qu'il faut lire ou écrire et ne pas faire les accès mémoire masqués. Une autre solution effectue tous les accès avant de savoir quels sont ceux à masquer, mais de ne pas déclencher d'exception avant qu'on sache quels sont les accès masqués. Une fois le masquage des accès effectués, le processeur exécute les exceptions adéquates. Il s'agit d'un comportement de masquage d'exception, très utilisé. L'implémentation est assez simple pour les accès mémoire contiguës, mais plus compliqué pour les accès en ''stride'' ou en ''scatter-gather''. Pour les accès contiguës, les seules exceptions pouvant survenir sont celles ayant lieu quand on passe d'une page mémoire à la suivante (au sens pagination, mémoire virtuelle). Et le hardware pour détecter un débordement de page est assez simple. Mais pour les accès en ''stride'' ou en ''scatter-gather'' demandent qu'oin vérifie les exceptions pour chaque élément du vecteur. ===L'implémentation matérielle=== L'implémentation matérielle est assez proche des processeurs SIMD purs. On retrouve plusieurs unités de calcul travaillant en parallèle, avec quelques circuits ajoutés pour gérer la prédication. Le cout supplémentaire en circuits est acceptable, les gains en performance associés sont très intéressants. Les circuits en plus, pour gérer la prédication, sont similaires à ceux utilisés pour la prédication sur les architectures non-SIMD, mais sont dupliqués. La seule innovation architecturale est l'implémentation du registre de masque. Tous les processeurs n'en utilisent pas, mais c'est la solution la plus performante. En effet, ce registre de masque est un peu l'équivalent du registre d'état pour les instructions SIMD, bien qu'il soit techniquement une concaténation de registres à prédicat non-nommés. Ils ont pour avantage de libérer des registres SIMD normaux tout en ayant un cout en matériel très limité. Un autre point est que pour gérer les masques, les instructions SIMD à prédicat doivent lire le masque. Sans registre de masque, en utilisant un registre SIMD normal, il faut rajouter un troisième port de lecture sur le banc de registre pour récupérer le masque, ce qui est couteux en matériel. Pas besoin avec un registre de masque, qui n'est connecté qu'à l'ALU, sur le même modèle que le registre d'état. Plus haut, on a vu que l'implémentation de l'ALU SIMD peut se faire en utilisant des moins d'ALU que prévu. Par exemple, pour des vecteurs de 32 éléments, on peut utiliser 16 ALUs (allant à double fréquence ou non). Une instruction SIMD est alors décodée en plusieurs micro-instructions SIMD consécutives, chacune traitant une partie d'un vecteur. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. L'avantage est que cela se marie bien avec l'abandon des opérations pour les masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit vérifier que les 8 bits de poids faible ainsi que de poids fort du registre de masque ne soient pas individuellement à zéro. Cela se fait en dupliquant la porte NOR évoquer plus haut, l'une pour la partie basse du registre et la seconde pour la partie haute. ==La prédication pour les structures de contrôle imbriquées== Au niveau du jeu d’instruction, les architectures SIMT implémentent de la prédication, sous une forme améliorée. Les processeurs SIMT actuels sont surtout utilisées sur les processeurs intégrés aux cartes graphiques. Et ces derniers gèrent très mal les branchements, et encore : beaucoup de cartes graphiques, même récentes, ne gèrent tout simplement pas les branchements. Elles doivent donc se débrouiller avec uniquement la prédication, là où les processeurs SIMD utilisent des branchements normaux en complément de la prédication. Insistons sur le fait que cet usage exclusif de la prédication n'est présent que sur une sous-partie des architectures SIMT, le seul exemple que l'auteur de ce wikilivre connait étant celui des cartes graphiques. Les architectures SIMT sans branchements doivent donc trouver des solutions pour gérer les structures de contrôle imbriquées, à savoir une boucle placée à l'intérieur d'une autre boucle, un IF...ELSE dans un autre IF...ELSE, etc. Elles utilisent pour cela la prédication, combinée avec des mécanismes annexes. Le premier d'entre eux est l'usage de plusieurs registres de masques organisés d'une manière bien précise, l'autre est l'usage de compteurs d'activité. Voyons ces deux techniques. ===La pile de masques=== La '''pile de masques''' remplace le ou les registres de masque. Sans elle, le processeur SIMD incorpore un registre de masque qui est adressé implicitement ou explicitement. Éventuellement, le processeur peut contenir plusieurs registres de masque séparés adressables via un nom de registre. Avec elle, le processeur SIMD incorpore plusieurs registres de masque organisé en pile. Le registre de masque est donc remplacé par une mémoire LIFO, une pile, dans laquelle plusieurs masques sont empilés. Le tout forme une pile, similaire à la pile d'appel, sauf qu'elle est utilisée pour empiler des masques. Un masque est calculé et empilé à chaque entrée dans une structure de contrôle, puis dépilé une fois la structure de contrôle exécutée. L'empilement et le dépilement des masques est effectué par des instructions PUSH et POP, présentes dans le jeu d'instruction du processeur SIMD. Le calcul des masques doit répondre à plusieurs impératifs. * Premièrement, chaque masque se calcule en faisant un ET entre le masque précédent et le masque calculé par l'instruction de test. Cela permet de ne pas réveiller d’élément au beau milieu d'une structure imbriquée. Si in IF désactive certains éléments du vecteur, une condition imbriquée dans ce IF ne doit pas réveiller cet élément. Le fait de faire un ET entre les masques garantit cela. * Deuxièmement, les masques doivent être empilés et dépilés correctement. Au moment de rentrer dans une structure de contrôle, on effectue une instruction de test associée à la structure de contrôle, qui calcule un masque, et on empile le masque calculé. Au moment de sortir de la structure de contrôle, on dépile le masque en question. L'implémentation demande d'utiliser une mémoire LIFO pour stocker la pile de masques, et quelques circuits annexes. Il faut notamment un circuit relié à l'ALU qui récupère les conditions, les résultats des comparaisons, et qui effectue le ET pour combiner les maques. Pour donner un exemple, prenons le code suivant, qui est volontairement simpliste et ne sert qu'à des fins d'explication : <syntaxhighlight lang="c"> if ( condition 1 ) { if ( condition 2 ) { ... } else { ... } Autres instructions } Instructions après le IF... </syntaxhighlight> Imaginons que l'on traite des vecteurs de 8 éléments. Pour le vecteur considéré, la première condition (a > 0) n'est respectée que par les 4 premiers éléments. L'instruction de condition calcule alors le masque correspondant : 1111 0000. Le masque est alors calculé, puis empilé au somment de la pile. La seconde instruction de test, qui teste la variable b, est maintenant valide pour les 4 bits du milieu du masque. Mais n'allez pas croire que le masque correspondant soit 0011 11100 : il faut tenir compte de la condition précédente, qui a éliminé les 4 derniers éléments. Pour cela, on fait un ET logique entre le masque précédent, et le masque calculé par la condition. Le masque au sommet de la pile est donc lu, combiné avec le masque calculé par l'instruction, ce qui donne le masque final. Le masque final est alors empilé au sommet de la pile. On exécute alors l'instruction du IF, en tenant compte du masque qui est au sommet de la pile. Si le IF était plus compliqué, toutes les instructions suivantes tiendraient compte du masque. En fait, le masque est pris en compte tant qu'il n'est pas dépilé. Une fois que le IF est terminé, le masque est dépilé. On passe alors au ELSE, et rebelotte. Le masque pour le ELSE est calculé en combinant le masque au sommet de la pile avec la condition du ELSE. Le masque au sommet de la pile est celui calculé à l'entrée du premier IF, pas le second qui a été dépilé. Les instructions du ELSE sont alors exécutées en tenant compte de ce masque. Une fois qu'elles sont toutes exécutées, le masque est dépilé. Puis vient l'exécution des instructions après le ELSE. Elles utilisent le masque empilé au sommet de la pile, qui correspond à celui à l'entrée du IF. Puis vient le moment d'exécuter les instructions après le IF : pas de masque, on exécute sur tout le vecteur. ===Les compteurs d'activité=== Une variante de la technique précédente remplace la pile de masques par des '''compteurs d'activité'''. La technique est similaire, si ce n'est qu'elle utilise moins de circuits. Avant , on avait une pile de masques de même taille, dont les bits sont à 0 ou 1 suivant que la condition est remplie. La pile de masque ressemble donc à ceci : {|class="wikitable" |- ! masque 1 | 1 || 1 || 1 || 1 |- ! masque 2 | 0 || 1 || 1 || 1 |- ! masque 3 | 0 || 1 || 1 || 1 |- ! masque 4 | 0 || 0 || 0 || 1 |- ! masque 1 | colspan="4" | vide |} Une manière équivalent de représenter cette pile de masque est de compter combien de bits sont à 0 dans chaque colonne. Attention : j'ai bien dit à 0 ! On obtient alors : {|class="wikitable" |- ! masque 1 | 3 || 1 || 1 || 0 |} Et c'est le principe caché derrière la techniques des compteurs d'activité. Chaque élément dans un vecteur, chaque place, se voit attribuer un compteur. Un compteur non-nul indique qu'il ne faut pas prendre en compte l’élément. Ce n'est qu'une fois que le compteur est nul que l'on effectue des opérations sur l’élément associé du vecteur. À chaque fois qu'on entre dans une structure de contrôle, on teste une condition sur chaque élément. Si la condition est respectée pour un élément, alors le compteur ne change pas. Mais si la condition n'est pas respectée, alors on incrémente le compteur associé. En sortant de la structure de contrôle, on décrémente le compteur associé. Notons que les compteurs qui n'ont pas été incrémenté en entrant dans la structure de contrôle ne sont pas décrémenté en sortant. En clair, là où on empilait/dépilait un masque, on se contente d'incrémenter/décrémenter un compteur. Utiliser un compteur en lieu et place d'une colonne entière dans la pile de masque utilise moins de bits. Et c'est sans doute pour cette raison que certaines cartes graphiques, comme les cartes graphiques intégrées d'Intel depuis 2004, utilisent cette technique. ==Les processeurs vectoriels== Les '''processeurs vectoriels''' sont les ancêtres des processeurs à instruction SIMD. Bien qu'ils soient arrivés en premier, ils incorporent diverses techniques que de simples instructions SIMD n'ont pas forcément. C'est paradoxal, mais c'est ainsi. La première différence se manifeste au niveau du jeu d'instruction. Les processeurs vectoriels sont capables de traiter des vecteurs de taille variable, alors que les instructions SIMD usuelles ne gèrent que des vecteurs de taille fixe. Par taille variable, on veut dire que le processeur gère nativement des vecteurs d'une taille allant de 1 à la taille maximale d'un vecteur. La taille maximale d'un vecteur est celle permise par la taille des registres vectoriels, il s'agit de la taille d'un vecteur SIMD équivalent sur les autres processeurs SIMD. La seconde différence est une différence en termes de micro-architecture. Un processeur SIMD actuel dispose d'une unité de calcul très élaborée, capable de faire plusieurs calculs en parallèle, au moins un pour chaque élément du vecteur. Par exemple, si un vecteur contient 16 entiers, l'ALU SIMD doit contenir au moins 16 additionneurs entiers. Mais sur un processeur vectoriel, ce n'est pas le cas : l'unité de calcul est en réalité une unité de calcul entière/flottante normale, mais qui a la particularité d'être pipelinée. Les calculs sont donc effectués en parallèle, mais d'une manière totalement différente. La plupart de ces différences s'expliquent par le fait que les anciens processeurs vectoriels devaient faire avec des limitations en termes de circuits. Ils avaient peu de transistors à leur disposition, ce qui fait que leur unité de calcul devait être la plus simple possible. D'où l'utilisation d'une unité de calcul simple, mais utilisée de manière pipelinée. De plus, les processeurs vectoriels ne possèdent aucune mémoire cache pour les données et se contentent juste de caches d'instruction. De tels caches sont généralement peu utiles quand on manipule des tableaux, chose quasiment systématique quand on travaille avec du parallélisme de données. ===Une unité de calcul pipelinée=== La différence entre processeur vectoriel et SIMD tient dans la façon dont sont traités les vecteurs : les instructions SIMD traitent chaque élément en parallèle, alors que les processeurs vectoriels pipelinent ces calculs ! Par pipeliner, on veut dire que l’exécution de chaque instruction est découpée en plusieurs étapes indépendantes. Au lieu d'attendre la fin de l’exécution d'une opération avant de passer à la suivante, on peut commencer le traitement d'une nouvelle donnée sans avoir à attendre que l'ancienne soit terminée. [[File:Pipeline.png|centre|vignette|upright=2|Pipeline vectoriel.]] Pour donner un exemple, on peut donner l'exemple d'une multiplication flottante effectuée entre deux registres. Son exécution peut être décomposée en plusieurs étapes. Par exemple, on peut avoir 3 étapes : * une première étape E qui va additionner les exposants et gérer les diverses exceptions ; * une étape M qui va multiplier les mantisses ; * et enfin une étape A qui va arrondir le résultat. L’exécution de notre opération flottante sur un vecteur donnerait donc quelque chose dans le genre, où chaque ligne correspond au traitement d'un nouvel élément dans un vecteur, dans un paquet SIMD. [[File:Multiplication vectorielle - pipeline.png|centre|vignette|upright=2|Multiplication vectorielle - pipeline]] : Certains processeurs vectoriels utilisent plusieurs ALU pipelinées pour accélérer les calculs. On peut les voir comme des intermédiaires entre processeur vectoriels et SIMD SWAR. Avec une unité de calcul pipelinée découpée en N étages, on peut gérer N données simultanées : autant qu'il y a d'étapes différentes. Mais ce nombre maximal de données met un certain temps avant d'être atteint. L'unité de calcul met du temps avant d'arriver à son régime de croisière. Durant ce temps, elle n'a pas commencé à traiter suffisamment d’éléments pour que toutes les étapes soient occupées. Ce temps de démarrage est strictement égal du nombre d'étapes nécessaires pour effectuer une instruction. La même chose arrive vers la fin du vecteur, quand il ne reste plus suffisamment d’éléments à traiter pour remplir toutes les étapes. [[File:Startup and dead time vector pipeline.png|centre|vignette|upright=2|Startup and dead time vector pipeline]] Pour amortir ces temps de démarrage et de fin, certains processeurs démarrent une nouvelle instruction sans attendre la fin de la précédente : deux instructions peuvent se chevaucher pour remplir les vides. Par contre, il peut y avoir des problèmes lorsque deux instructions qui se suivent manipulent le même vecteur. Il faut que la première instruction ait fini de manipuler un élément avant que la suivante ne veuille commencer à le modifier. Pour cela, il faut avoir des vecteurs suffisamment qui contiennent plus d’éléments qu'il n'y a d'étapes pour effectuer notre instruction. ===La technique du ''chaining''=== La technique du pipeline peut encore être améliorée dans certains cas particuliers. L'idée est simplement d'ajouter la technique du contournement (''bypass'') à l'unité de calcul. Pour rappel, le contournement connecte la sortie de l'ALU à une de ses entrées, ce qui permet au résultat d'un calcul d'être réutilisable au cycle d’horloge suivant. L'usage du contournement sur les processeurs vectoriels porte le nom de '''''Vector Chaining'''''. Un processeur implémentant le ''chaining''/''bypass'' a toutes ses unités de calcul reliées entre elles : la sortie d'une unité est reliée aux entrées de toutes les autres. Pour comprendre à quoi peut servir cette technique, on peut citer deux grands exemples principaux. Le premier avantage est que l'implémentation des instructions SIMD horizontales est beaucoup plus simple. C'est pour cela que les processeurs vectoriels intègrent souvent des instructions horizontales, et notamment des instructions arithmétiques horizontales. Le contraste avec les processeurs SIMD récents, qui utilisent plusieurs ALU travaillant en parallèle, est frappant. Ces derniers n’intègrent pas d'instructions horizontales arithmétiques, les seules opérations horizontales supportées sont généralement des permutations ou des instructions ''compress/expand'', guère plus. Maintenant, introduisons une autre possibilité par un exemple. Dans cet exemple, on suppose que le processeur dispose d'une ALU pour l'addition et d'une autre pour la multiplication. Imaginons que l'on ait trois vecteurs nommés A, B et C. Pour chaque énième élément de ces paquets, je souhaite effectuer le calcul <math>A_n + B_n \times C_n</math>. En théorie, il faudrait faire d'abord la multiplication, stocker le résultat temporaire de la multiplication dans un registre vectoriel, puis faire l'addition. Mais en rusant un peu, on peut utiliser le pipeline plus efficacement. Une fois que le premier élément de la multiplication du premier vecteur est connu, pourquoi ne pas démarrer l'addition immédiatement après, et continuer la multiplication en parallèle ? Après tout, les deux calculs ont lieu dans des ALUs séparés. Et le contournement permet d'alimenter l'ALU d'addition avec la sortie du circuit multiplieur. [[File:Vector chaining.png|centre|vignette|upright=2|Vector chaining]] ===La gestion des vecteurs de taille variable=== L'usage d'une unité de calcul pipelinée fait que la taille des vecteurs n'est pas forcément fixe. Autant les processeurs SIMD non-vectoriels disposent d'un nombre fixe d'unités de calcul, et peuvent donc gérer des vecteurs de taille fixe, autant ce n'est pas le cas des processeurs vectoriels. Une unité de calcul pipelinée à 16 étage peut utiliser les trois premiers pour traiter un vecteur de 3 élements, les 5 suivants pour un second vecteur de 5, et le reste pour un troisième vecteur. Reste que la gestion des vecteurs de taille variable doit être gérée au niveau du séquenceur, mais surtout : doit être permise au niveau du jeu d'instruction. Pour cela, il faut ajouter au jeu d'instruction de quoi indiquer la taille du vecteur en cours de traitement. Et c'est le rôle d'un registre spécialisé, le '''Vector Length Register''', que de gérer les vecteurs de taille variable. Le ''Vector Length Register'' est un registre qui indique combien d’éléments on doit traiter dans un vecteur. On peut ainsi dire au processeur : je veux que tu ne traites que les 40 premiers éléments présents d'un paquet. Ils sont utilisés pour gérer des tableaux dont la taille n'est pas un multiple d'un vecteur. Quand on arrive à la fin d'un tableau, il suffit de configurer le ''Vector Length Register'' pour ne traiter que ce qu'il faut. [[File:Vector length register.png|centre|vignette|upright=2|Vector length register.]] Il facilite l'usage du '''déroulage de boucle''', une optimisation qui vise à réduire le nombre d'itérations d'une boucle en dupliquant son corps en plusieurs exemplaires. Le compilateur réplique le corps de la boucle (les instructions à répéter) en plusieurs exemplaires dans cette boucle, avant de corriger le nombre d'itérations de la boucle. Par exemple, prenons cette boucle, écrite dans le langage C : <syntaxhighlight lang="c"> int i; for (i = 0; i < 100; ++i) { a[i] = b[i] * 7 ; } </syntaxhighlight> Celle-ci peut être déroulée comme suit : <syntaxhighlight lang="c"> int i; for (i = 0; i < 100; i+=4) { a[i] = b[i] * 7 ; a[i+1] = b[i+1] * 7 ; a[i+2] = b[i+2] * 7 ; a[i+3] = b[i+3] * 7 ; } </syntaxhighlight> Les instructions vectorielles permettent de traiter plusieurs éléments à la fois, ce qui fait que plusieurs tours de boucles peuvent être rassemblés en une seule instruction SIMD. Le déroulage de boucles permet d'exposer des situations où ce regroupement est possible. Dans notre exemple, si jamais notre processeur dispose d'une instruction de multiplication capable de traiter 4 éléments du tableau a ou b en une seule fois, la boucle déroulée peut être vectorisée assez simplement en utilisant une multiplication vectorielle (que nous noterons vec_mul). <syntaxhighlight lang="c"> int i; for (i = 0; i < 100; i+=4) { vec_a[i] = vec_mul ( vec_b[i] , 7 ) ; } </syntaxhighlight> Le déroulage de boucles n'est toutefois pas une optimisation valable pour toutes les boucles. Reprenons l'exemple de la boucle vue plus haut. Si jamais le tableau à manipuler a un nombre d’éléments qui n'est pas multiple de 4, la boucle ne pourra être vectorisée, vu que la multiplication vectorielle ne peut traiter que 4 éléments à la fois. Pour ce faire, les compilateurs utilisent généralement deux boucles : une qui traite les éléments du tableau avec des instructions SIMD, et une autre qui traite les éléments restants avec des instructions non vectorielles. Cette transformation s'appelle le '''strip-mining'''. Par exemple, si je veux parcourir un tableau de taille fixe contenant 102 éléments, je devrais avoir une boucle comme celle-ci : <syntaxhighlight lang="c"> int i; for (i = 0; i < 100; i+=4) { a[i] = b[i] * 7 ; a[i+1] = b[i+1] * 7 ; a[i+2] = b[i+2] * 7 ; a[i+3] = b[i+3] * 7 ; } for (i = 100; i < 102; ++i) { a[i] = b[i] * 7 ; } </syntaxhighlight> Les processeurs vectoriels utilisent le ''Vector Length Register'' pour éviter d'utiliser le strip-mining. Avec ce registre, il est possible de demander aux instructions vectorielles de ne traiter que les n premiers éléments d'un vecteur : il suffit de placer la valeur n dans le ''Vector Length Register''. Évidemment, n doit être inférieur au nombre d’éléments maximal du vecteur. Avec ce registre, on n'a pas besoin d'une seconde boucle pour traiter les éléments restants, et une simple instruction vectorielle peut suffire. L'avantage principal est que cela réduit la portion de code non-parallélisable, donc augmente l'efficience SIMD. Mais l'intérêt n'est pas qu'une question de code. Il faut aussi prendre en compte que cela permet d'utiliser l'ALU plus efficacement. Dès qu'un vecteur court est terminé, le processeur peut immédiatement démarrer le calcul d'un autre vecteur, le vecteur suivant, l'instruction suivante, sans laisser de cycles inutilisés. Avec un processeur SIMD non-vectoriel, on aurait du utiliser de la prédication, donc utiliser seulement une partie des ALU SIMD, ce qui aurait été un gâchis. ===Les accès mémoire=== Les accès mémoire des processeurs vectoriels sont sensiblement les mêmes que ceux des autres processeurs SIMD. Les instructions d'accès mémoire sont les mêmes, on retrouve les modes d'adressages précédents, dont les accès en ''scatter-gather'' et en ''stride''. Sur certains processeurs vectoriels, les instructions vectorielles lisent et écrivent en mémoire RAM, sans passer par des registres : on parle de '''processeurs vectoriels mémoire-mémoire'''. Elles n'avaient pas d'instructions LOAD et STORE, les instructions arithmétiques géraient directement la lecture des opérandes en mémoire, et incrémentaient automatiquement les adresses à lire/écrire. Elles pouvaient gérer des vecteurs de taille arbitraire, la gestion d'un tableau se faisait parfois en une seule instruction ! Le problème est que l'absence de registres pour stocker les données lues réduisait grandement les performances. [[File:Processeur vectoriel mémoire-mémoire.png|centre|vignette|upright=2|Processeur vectoriel mémoire-mémoire]] Les processeurs vectoriels mémoire-mémoire étaient cependant minoritaires, la grosse majorité des processeurs vectoriels récents possèdent des registres vectoriels dédiés aux vecteurs. La plupart sont des architectures de type LOAD-STORE, mais pas toute. De fait, il n'y a pas de lien strict entre adressage des opérandes et processeurs vectoriel (ou SIMT). Seuls les processeurs SIMD de type SWAR ont une restriction là-dessus et sont systématiquement des architectures LOAD-STORE. [[File:Processeur vectoriel à registres vectoriels.png|centre|vignette|upright=2|Processeur vectoriel à registres vectoriels.]] Il faut noter que l'implémentation des accès mémoire est potentiellement différente de celle des autres processeurs SIMD. En principe, on peut utiliser une implémentation identique entre les deux. Mais le fait que les processeurs vectoriels sont pipelinés fait qu'il est préférable d'utiliser une implémentation différente, qui se base sur une RAM ou des caches pipelinés. Vu que l'ALU reçoit une/deux opérandes par cycle, l'idéal est que la mémoire ou le cache aillent au même rythme et soient donc pipelinés.Bien sur, il est possible d'utiliser une implémentation plus classique, avec des caches multiports, un chargement en plusieurs fois des vecteurs depuis le cache, des circuits pour réorganiser les accès mémoire, etc. En théorie, les architectures vectorielles font peu usage de mémoires cache. Les transferts entre registres vectoriels et mémoire RAM sont directs, sans intermédiaire. La raison est que les applications qui tirent parti du SIMD ont une bonne localité spatiale, mais une mauvaise localité temporelle. Or, les caches de données profitent surtout de la localité temporelle pour donner de bonnes performances. De plus, rappelons que les processeurs vectoriels sont assez anciens et datent d'une époque où les transistors étaient précieux. Il valait mieux utiliser des transistors dans un gros cache d'instruction que pour un cache de donnée peu efficace. ==Les systèmes SIMD à plusieurs processeurs : le SIMT== Enfin, terminons avec le dernier type de SIMD, qui date du tout début de l'informatique : celui où on combine plusieurs processeurs non-SIMD. L'idée est de synchroniser plusieurs processeurs de manière à ce qu'ils exécutent la même instruction en même temps, chacun sur des données différentes. On parle d''''exécution ''lockstep''''' des instructions. Toute la difficulté est de garantir la synchronisation des processeurs, garantir que tous les processeurs exécutent la même instruction. Ce qui pose problème quand des branchements sont impliqués, des processeurs pouvant prendre le branchement et pas les autres. ===Les ''array processors'' des temps anciens=== Les premières architectures matérielles conçues pour le parallélisme de données étaient de ce type. On peut par exemple citer l'exemple des Thinking machines CM-1 et CM-2, qui exécutaient tous la même instruction au même cycle d'horloge sur 64000 processeurs minimalistes. Un autre exemple est celui de l'ILLIAC IV. Historiquement, le terme utilisé pour désigner de telles architectures était '''array processors''', encore que ce terme fût réservé aux systèmes multiprocesseurs. Par la suite, le terme ''Single Instruction Multiple Threads'', ou SIMT, a été utilisé. Mais ce terme a ensuite été repris par le marketing de NVIDIA pour désigner une technique précise de SIMD avec prédication, utilisée sur ses cartes graphiques récentes. Le mauvais usage de la terminologie fait que le terme SIMT est devenu polysémique et quelque peu trompeur. L'ILLIAC 4 était un ordinateur un peu particulier, presque unique en son genre. Son architecture était composé d'un processeur principal couplé à plusieurs processeurs secondaires. Il y avait en tout un processeur principal et 64 processeurs secondaires. Le processeur principal chargeait les instructions depuis la mémoire, les décodaient, puis envoyait les instructions aux processeurs secondaires. Le processeur principal était le seul à avoir un ''program counter'', il était utilisé pour gérer les boucles, ainsi que pour configurer les instructions à prédication. Cependant, le processeur principal n'était pas un simple séquenceur : il pouvait exécuter des instructions, avait un accumulateur et des registres, etc. Il prenait en charge toute instruction non-SIMD, alors que les instructions SIMD étaient envoyées aux processeurs secondaires. Les 64 processeurs secondaires avaient chacun : une unité de calcul, un ''local store'' de 2048 flottants de 64 bits, une unité mémoire. Ils n'ont pas de séquenceur ni de ''program counter'', ni de quoi charger une instruction. L'unité de calcul est appelée un ''processing element'' ou PE. Elle intègre un additionneur flottant, de quoi faire des décalages, et une unité de calcul logique. De plus, un PE intègre des registres accumulateurs et de quoi calculer des adresses : un registre d'indice, un registre d'adresse, un circuit de calcul d'adresse. L'unité mémoire connectait l'ALU entière PE et le ''local store'', et elle permettait aussi d'adresser des entrées-sorties. Pour gérer la prédication, les processeurs secondaires pouvaient être activés ou désactivés suivant le résultat des branchements/conditions. Intuitivement, on peut faire le rapprochement avec un processeur SIMD. Le processeur principal regrouperait la partie non-SIMD d'un processeur SIMD, alors que les processeurs secondaires seraient l'unité de calcul SIMD. Cependant, il y a une différence très importante : il n'y a pas de banc de registre vectoriel. À la place, chaque processeur secondaire a son propre banc de registre, et ce sont des registres scalaires ! Une autre différence est que la mémoire RAM est découpée en plusieurs ''local store'' de 2048 adresses, chacun étant relié à une unité de calcul PE. N'oublions pas le système de calcul d'adresse et l'unité LOAD/STORE qui est fusionnée avec l'unité SIMD, chaque PE ayant ses circuits de calcul d'adresse, sa propre unité mémoire, etc. ===Les anciennes cartes graphiques hybrides SIMD-VLIW=== Une technique de SIMD multi-processeur a été utilisée autrefois sur certaines cartes graphiques AMD à architecture Terascale. La différence est que l'architecture en question était multicœur et non multi-processeurs. Elles contenaient plusieurs cœurs VLIW, qui fonctionnaient en ''lockstep''. Un groupe de 16 processeurs VLIWs exécutaient la même opération VLIW. Du moins, c'est ce que semblent dire les [https://www.techpowerup.com/gpu-specs/docs/amd-gcn1-architecture.pdf white papers de présentation de l'architecture GCN]. L'usage de cet hybride SIMD-VLIW était très adapté au traitement graphique. Les cartes graphiques post-années 2000 sont capables d'exécuter des programmes appelés des '''''shaders''''', qui sont utilisés en rendu 3D, pour calculer certaines scènes 3D, et notamment pour calculer les ombres (d'où leur nom de shader, pour shade - ombre). Or, ces shaders sont justement exécutés par la carte graphique, sur les processeurs de shaders. Un processeur de ''shader'' applique un programme/''shader'' sur chaque ''pixel'' ou triangle d'une scène 3D (en fait des fragments et des vertices, mais faisons cette simplification). Chaque triangle ou pixel d'une scène 3D peut être traité indépendamment des autres, ce qui rend le traitement 3D fortement parallèle. Aussi, l'usage du SIMD est assez naturel : chaque processeur exécute un ''shader'' sur un pixel/triangle, utiliser une architecture SIMD parait naturel. Reste à justifier l'usage de plusieurs CPU VLIW. La raison tient dans la manière dont est traité un pixel par un shader. Un pixel est codé en utilisant quatre informations. Premièrement, une couleur codée avec trois nombres flottants, qui représentent une couleur au format RGB avec un mélange de rouge, vert et bleu, chaque couleur rouge/bleu/vert étant codée avec un flottant de 32 bits. La quatrième information est un nombre qui code la transparence du pixel. Généralement, le shader traite différemment la transparence de la couleur RGB, dans le sens où les instructions utilisées ne sont pas les mêmes. Au lieu de traiter couleur RGB et transparence séparément, elles sont traitées en même temps en parallèle, ce qui demande de pouvoir émettre deux instructions : une pour le calcul de la transparence, une autre pour la couleur RGB. Fait amusant, l'instruction utilisée pour gérer la composante RGB est une instruction vectorielle qui travaille sur des vecteurs de 3 couleurs. Les architectures VLIW sont parfaites pour cela. Les cartes graphiques de l'époque de DirectX 9 utilisaient cette méthode qui mélangeait SIMD et VLIW. Elles disposaient de plusieurs cœurs VLIW qui exécutaient le même shader en exécution ''lockstep''. Chaque cœur traitait au moins un pixel à la fois, ou un triangle à la fois. Cependant, nous devons préciser que les cœurs VLIW partagent la même unité de contrôle, ce sont en réalité des chemins de données plus qu'autre chose. L'architecture était aussi combinée avec du ''Fine Grained Multithreading'' afin de pouvoir gérer les accès mémoire, de telles architectures ne pouvant pas se permettre d'utiliser l'exécution dans le désordre. De telles architectures multicœurs à exécution ''lockstep'' permettent quelques optimisations, comme le fait de partager le cache d'instruction entre les cœurs. ==Résumé des différents types de processeurs SIMD== Dans ce chapitre, nous avons appris qu'il existe plusieurs catégories de processeurs SIMD. Les différences entre ces catégories tiennent à la fois dans le jeu d'instruction, mais aussi dans la microarchitecture des processeurs en question. Au niveau du jeu d'instruction, il y a trois paliers, qui sont résumés dans le tableau ci-dessous. {|class="wikitable" |- ! ! Support de la prédication ! Taille des vecteurs ! Instructions horizontales ! Architecture LOAD-STORE ! Pointeur de pile |- ! Processeurs SIMD purs | Non | rowspan="3" | Fixe | rowspan="3" | Limitées à des échanges de données à l'intérieur d'un vecteur, pas d'instructions de réduction | Oui | rowspan="2" | Unique |- ! Processeurs SIMD avec prédication | Oui, par définition | rowspan="3" | Dépend du processeur |- ! Processeurs SIMT | rowspan="2" | Oui, sauf exceptions | Un pointeur de pile possible par élément du vecteur |- ! Processeurs vectoriels | Variable | Toutes supportées, y compris les instructions arithmétiques horizontales | Unique |} Au niveau de la microarchitecture, il y a trois paliers, qui sont résumés dans le tableau ci-dessous. {|class="wikitable" |- ! ! Unité de calcul ! Banc de registre ! Adressage des opérandes |- ! Processeurs SIMD | rowspan="2" | Plusieurs ALU simples non-pipelinée, plusieurs additionneurs/multiplieurs/autres | Banc de registres vectoriels | Architecture LOAD-STORE |- ! Processeurs SIMT | Plusieurs bancs de registres scalaires (non-vectoriels, registres normaux) | rowspan="2" | Dépend du processeur |- ! Processeurs vectoriels | ALU simple unique, pipelinée | Banc de registres vectoriels |} Les processeurs SIMD purs ont des performances différentes des processeurs vectoriels, vu que les premiers font leurs calculs en parallèle, alors que les CPU vectoriels utilisent un pipeline. Mais dans les grandes lignes, les performances sont similaires. Les différences sont grandement atténuées par l'usage du ''vector chaining'', de la gestion des vecteurs de taille variables, et des autres optimisations spécifiques aux processeurs vectoriels. L'avantage est cependant du côté des processeurs SIMD pur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Architectures multithreadées et Hyperthreading | prevText=Architectures multithreadées et Hyperthreading | next=La cohérence des caches | nextText=La cohérence des caches }} {{autocat}} </noinclude> n6yn1py6vjq8as6v1doeqragwvn3eya Fonctionnement d'un ordinateur/Architectures multithreadées et Hyperthreading 0 65961 773845 771170 2026-10-01T12:18:34Z Mewtow 31375 Correction terme fenêtre d'instruction 773845 wikitext text/x-wiki Vous pensez surement qu'il faut obligatoirement plusieurs cœurs pour exécuter plusieurs programmes en parallèle, mais sachez que c'est faux ! Les processeurs mono-cœur en sont capables, en alternant entre les programmes à exécuter. Plusieurs programmes s’exécutent donc sur le même processeur, mais chacun à leur tour et non en même temps. D'ordinaire, cette alternance est gérée par le système d'exploitation, mais certains processeurs gèrent cette alternance eux-mêmes, directement au niveau matériel. L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un programme est bloqué par un accès mémoire, le processeur donne la main à un autre programme. Les processeurs en question sont appelés des processeurs multithreadés, ou encore des '''architectures multithreadées''', en référence au terme ''thread'', qui est plus ou moins équivalent à celui de programme dans ce cours. Ils exécutent un ''thread'' à la fois, mais changent de ''thread'' plus ou moins régulièrement. Pour comprendre le pourquoi des architectures multithreadées, il faut rappeler qu'il arrive que l'unité de calcul d'un processeur ne fasse rien, par exemple pendant que le processeur accède à la mémoire. Les cycles d'horloge où l'unité de calcul est inutilisée sont des '''cycles gâchés'''. L’exécution dans le désordre réduit ces cycles gâchés, mais les architectures multithreadées sont une solution alternative et complémentaire. Elles visent à ce que les cycles gâchés d'un ''thread'' soient remplis par les calculs d'autre ''thread''. Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Le ''Simultaneous MultiThreading'' est spécifique aux processeurs superscalaires, alors que les autres techniques fonctionnent sur tous les processeurs, qu'ils soient superscalaire ou simple-émission. : Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. ==Le multithreading temporel== Le FGMT et le CGMT sont regroupés sous le terme de '''multithreading temporel'''. Les deux partagent en effet un même point commun : à chaque cycle, les instructions émises par l'unité d'émission proviennent d'un seul programme. La distinction n'a de sens que sur les processeurs à exécution multiple, ce sera plus clair dans la suite du chapitre quand on comparera ''multithreading'' temporel et SMT. La différence entre FGMT et CGMT est la fréquence des changements de ''thread'' : à chaque cycle ou presque pour le FGMT, lors d'un évènement bien précis pour le CGMT. Le processeur subit des changements pour gérer plusieurs ''threads'' en cours d'exécution. En premier lieu, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement''', dont le fonctionnement dépend du processeur, comme nous le verrons plus bas. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''. [[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]] Ensuite, le processeur attribue un numéro à chaque ''thread'', appelé le ''thread ID''. Il y en autant que de ''threads'' exécutables simultanément par le processeur. Par exemple, si le processeur gère au maximum 8 ''threads'' simultanés, l'identifiant de ''thread'' va de 0 à 7 et est codé sur 3 bits. Le ''thread ID'' est propagé dans le pipeline, histoire de savoir à quel ''thread'' appartient l'instruction en cours. Il sert notamment pour adresser le banc de registre, mais aussi pour d'autres choses. Les processeurs à ''multithreading'' temporel sont généralement des processeurs sans exécution dans le désordre, et n'ont donc pas de renommage de registres. Il est donc obligatoire de dupliquer les registres, pour que chaque programme ait ses registres architecturaux rien qu'à lui. Cela demande soit un banc de registre par programme, soit un banc de registre commun géré par fenêtrage de registre (chaque programme ayant sa propre fenêtre de registres). Il faut aussi dupliquer les ''load-store queue'', pour séparer les lectures/écritures en attente de chaque ''thread''. Il est possible d'utiliser une ''load-store queue'' unique dans laquelle chaque lecture/écriture mémorise le ''thread ID'', pour identifier le ''thread'' qui l'a émise. [[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2.5|Aperçu de l'architecture d'un processeur multithreadé]] ===Le ''Coarse Grained Multithreading''=== [[File:Coarse Grained Multithreading.png|vignette|upright=1|Coarse Grained Multithreading.]] Le '''Coarse Grained Multithreading''' change de programme quand un évènement bien précis a lieu. L'évènement en question fait que l'ALU restera inutilisé pendant un moment : accès à la mémoire, branchements, etc. Sur certains processeurs CGMT, il y a une instruction précise pour changer de ''thread''. La commutation de ''thread'' est alors totalement ou partiellement décidée par le logiciel. Mais il s'agit là d'un cas particulier. Le cas le plus fréquent est de changer de ''thread'' lors d'un défaut de cache. Vu que l'accès à la RAM est quelque chose de très lent, il est intéressant d'exécuter des instructions d'un autre ''thread'' pour recouvrir l'accès à la RAM. L'idée est similaire à ce qu'on a avec les lectures non-bloquantes et/ou l'exécution dans le désordre : pendant que l'unité d'accès mémoire gère le défaut de cache, on alimente l'unité de calcul avec des calculs indépendants. Sauf qu'avec le ''multithreading'', les calculs proviennent d'un autre ''thread''. Un point important est que le cache doit être un cache non-bloquant, sans quoi le ''multithreading'' matériel ne fonctionne tout simplement pas. Par exemple, prenons une architecture qui change de ''thread'' à chaque défaut de cache. Si on veut supporter plus de deux ''threads'', il faut que plusieurs ''threads'' subissent un défaut de cache pour que le ''multithreading'' ait de l'intérêt, ce qui implique un cache non-bloquant. [[File:Multithreading et mitigation de la latence mémoire.png|centre|vignette|upright=1.5|Multithreading et mitigation de la latence mémoire.]] Avec le CGMT, on est certain que toutes les instructions en cours d’exécution appartiennent au même ''thread''. Quand il passe d'un ''thread'' à l'autre, le processeur attend naturellement que le pipeline soit complétement vidé avant de charger les instructions du ''thread'' suivant. Le processeur peut donc se débrouiller avec un simple '''registre de ''thread''''' qui mémorise le ''thread ID'' du ''thread'' en cours d'exécution. Le registre de ''thread'' est directement connecté à l'entrée d'adresse du banc de registre, pour gérer le fenêtrage de registres. Il est aussi connecté à la ''load-store queue'', et surtout au multiplexeur de choix de ''thread'', dans l'unité de chargement. [[File:Implementation du CGMT.png|centre|vignette|upright=2|Implémentation du CGMT]] L'unité d'ordonnancement détermine quel ''thread'' charger dans le pipeline, elle sélectionne un ''thread ID''. Pour faciliter son travail, cette unité contient un registre qui mémorise quels sont les ''threads'' actifs. Les ''threads'' bloqués par un défaut de cache sont marqués comme inactifs tant que le défaut de cache n'est pas résolu. Évidemment, dès qu'un défaut de cache est résolu, l'unité d'accès mémoire prévient l'unité de choix de ''thread'' pour que celui-ci marque le ''thread'' adéquat comme de nouveau actif. Le registre est composé de N bits sur un processeur qui gère N threads maximum : chaque bit est associé à un ''thread'' et indique s'il est actif ou non. ===Le ''Fine Grained MultiThreading''=== [[File:Fine Grained Multithreading.png|vignette|upright=1|''Barrel processor''.]] Le '''''Fine Grained Multithreading''''' regroupe deux types de processeurs différents. Le premier type est celui des '''''barrel processors''''', qui changent de programme à chaque cycle d'horloge. Idéalement, chaque étage du pipeline est utilisé par une instruction différente. L'avantage est que, à l'intérieur d'un ''thread'', une nouvelle instruction démarre quand la précédente est terminée. Le résultat d'une instruction est déjà dans les registres quand l'instruction suivante s’exécute. Il n'y a donc pas de dépendances entre instructions successives, les circuits liés aux dépendances de données sont fortement simplifiés, le réseau de contournement de l'ALU disparait, l'unité de prédiction de branchement disparait (car le résultat d'un branchement est connu avant que le même ''thread'' exécute sa prochaine instruction). Mais les accès mémoire sont gérés à part des autres instructions. Lorsque un ''thread'' effectue un accès mémoire, il est mis en pause et d'autres ''threads'' sont lancés à sa place. Prenons l'exemple d'un processeur qui gère 16 ''threads'' maximum : si l'un d'eux est mis en pause, le processeur changera de ''thread'' tous les 15 cycles au lieu de 16. Mais pour que cela fonctionne sans encombre, on est obligé d'avoir un nombre assez élevé de programmes en cours d’exécution. Pour donner un exemple, le processeur MTA était capable d'exécuter un 128 ''threads'' maximum, pour un pipeline de 21 étages. La marge est élevée, mais cela compense le fait que le processeur n'a pas de cache, avec des accès mémoire prenant 150-170 cycles d'horloge ! Le désavantage est que les registres étaient dupliqués 128 fois ! Le processeur avait près d'un millier de registres, ce qui est énorme pour l'époque. [[File:Full multithreading.png|vignette|upright=1|Processeur à ''Fine Grained Multithreading''.]] Utiliser un ''barrel processor'' au mieux demande donc qu'il y ait un grand nombre de ''threads'' en cours d'exécution. Mais quelques processeurs FGMT font autrement, en se rapprochant du CGMT. Ils peuvent exécuter un ''thread'' durant quelques cycles successifs, plus d'une dizaine, voire plus. Cependant, il s'agit de processeurs sans exécution dans le désordre, qui sont bloqués dès qu'ils tombent sur une instruction dépendante. Et justement, ils masquent ces blocages en changeant de thread au lieu d'émettre des bulles de pipeline. La technique est beaucoup utilisée sur les cartes graphiques modernes, et sur certains processeurs spécialisés. Le désavantage est qu'ils doivent intégrer des ''scoreboards'' pour gérer les dépendances de données, sauf sur quelques processeurs laissaient la détection des dépendances au compilateur ! Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui introduit la technique de l''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite, avant de changer de ''thread''. Qu'il s'agisse des ''barrel processor'' ou des autres processeurs FGMT, tous ont une implémentation similaire. Contrairement aux processeurs CGMT, il n'y a pas de registre de ''thread ID'' unique. En effet, le processeur doit savoir, pour chaque instruction dans le pipeline, à quel ''thread'' elle appartient. Pour cela, les ''thread ID'' sont générés lors du chargement et propagés dans le pipeline en même temps que les instructions, où il est utilisé par le banc de registre, dans les ''load store queues'', etc. Pour résumer : propagation du ''thread ID'' dans le pipeline et disparition du registre de ''thread'' global. Avec le FGMT, le processeur charge et décode les instructions, avant de les placer dans plusieurs files d'instruction. Il y a une file d'instruction par ''thread''. Le choix de la file d'instruction est réalisé par un multiplexeur, commandé par une super-unité d'émission qui décide quel ''thread'' émet ses instructions (''thread issue unit''). [[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2.5|FGMT sur un processeur.]] L'unité d'émission en question contient un ''scoreboard'' par ''thread'', et combine leurs résultats pour décider quel ''thread'' émet une instruction. Sur les ''barrel processor'', le ''scoreboard'' ne gère que les dépendances liées aux accès mémoire, pour gérer les ''threads'' mis en pause et les re-démarrer quand la lecture est terminée. Sur les autres processeurs, les ''scoreboard'' gèrent les dépendances entre instructions. Les unités de choix de ''thread'' fonctionnent différemment entre les ''barrel processors'' et ceux qui peuvent exécuter plusieurs instructions successives d'un même ''thread''. Pour ces derniers, l'implémentation demande une coopération entre l'unité d'émission et l'unité de chargement. Le processeur profite du fait que le chargement des instructions depuis la mémoire et leur exécution se font en même temps : le processeur charge des instructions pendant qu'il en exécute d'autres. Si un thread émet une instruction, ce même thread charge une instruction au même moment. Sauf si la file d'instruction du thread est déjà pleine, auquel cas un autre thread est choisi. Il est possible de tenir compte du contenu du ''scheduler'' pour décider quels ''threads'' sont éligibles au chargement. Il est possible de mettre en pause un ''thread'' si celui-ci accumule un peu trop d'instructions dans le ''scheduler''. C'est en effet signe que ce ''thread'' est bloqué par un accès mémoire, une instruction multicycle ou des dépendances de données. À l'inverse, il est possible de prioriser les ''threads'' qui n'ont presque aucune instruction dans le''scheduler''. Car ce sont des ''threads'' qui s'exécutent rapidement au point de vider le''scheduler''. L'idée est de charger en priorité les instructions du ''thread'' pour lequel la file d'instruction est la moins remplie. L'unité de choix du ''thread'' utilise cette information pour déterminer quel ''thread'' charger au prochain cycle. ==Le ''Simultaneous multithreading'' : une spécificité des processeurs superscalaires== Les deux techniques vues au-dessus peuvent s'adapter sur les processeurs à émission multiple, à savoir qui sont capables d'émettre plusieurs instructions simultanément. La seule contrainte est que l'implémentation du FGMT est compliquée sur les architectures superscalaires, ce qui fait que les ''barrel processors'' sont généralement des processeurs VLIW ou sans émission multiple. Mais il existe une technique de ''multithreading'' matériel spécialement pensée pour les processeurs à émission multiple : le ''Simultaneous multithreading''. La comprendre est assez simple si on la compare au FGMT et au CGMT. Avec le ''multithreading'' temporel, lors d'un cycle d'horloge, les instructions émises appartiennent au même ''thread'' matériel. Il n'y a pas de situation où une instruction du ''thread'' 1 est émise en même temps qu'une instruction du ''thread'' 2. {| |[[File:CGMT sur processeur superscalaire.png|vignette|upright=1.5|CGMT sur processeur superscalaire]] |[[File:FGMT sur processeur superscalaire.png|vignette|upright=1.5|FGMT sur processeur superscalaire]] |} Le '''Simultaneous Multi-Threading''', abrégé en SMT, permet d'émettre simultanément des instructions provenant de ''threads'' séparés. Elle fonctionne sur les processeurs superscalaires, mais n'est pas possible sur les processeurs VLIW. [[File:Simultaneous Multi-Threading.png|centre|vignette|upright=2|Simultaneous Multi-Threading]] ===L'implémentation matérielle du SMT=== Dans les faits, tous les processeurs SMT sont des processeurs à exécution dans le désordre. Non pas que ce soit obligatoire, juste que le cout d'implémentation est bien plus faible sur un processeur à exécution dans le désordre. L'implémentation du SMT prend un processeur à exécution dans le désordre, ajoute plusieurs ''program counter'' et les circuits adéquats. L'unité d'émission choisit quelles instructions envoyer aux ALU, en utilisant l'exécution dans le désordre : tant qu'elles sont indépendantes, elles peuvent s’exécuter en parallèle. Et deux instructions de deux ''threads'' différents sont indépendantes ! Il y a donc un lien étroit entre exécution dans le désordre et SMT ! Ironiquement, avec l'amélioration des techniques d'exécution dans le désordre, le SMT commence à perdre de sa superbe. Plus l'exécution dans le désordre est efficace, plus le SMT est inutile. Si l'exécution dans le désordre est trop efficace, les cycles gâchés sont trop rares pour que le SMT ne servent pas à grande chose. Quand les cycles gâchés représentent grand maximum 10% du temps d’exécution grâce à l'usage de l’exécution dans le désordre, le SMT n'a plus grand-chose à remplir et le second programme s’exécuterait trop lentement pour ça vaille le coup. Le SMT s'implémente généralement avec un seul ''scheduler'' et avec le renommage de registres. Le renommage de registres fait qu'on n'a pas à utiliser de fenêtrage de registres, juste à augmenter la taille du banc de registres. L'idée est que l'on concatène le ''thread ID'' au nom de registre avant de faire le renommage. Comme cela, on garantit que deux registres architecturaux identiques mais référencés dans des ''threads'' différents, correspondront à des registres physiques différents. Ainsi, l'unité d'émission a juste à vérifier les dépendances entre registres, pas besoin de gérer les ''thread ID''. En faisant cela, la logique d'émission est beaucoup plus simple. Par contre, les tables pour le renommage de registre sont dupliqués, avec autant de table que de ''thread'', chaque ''thread'' a la sienne. Comme avec le FGMT, en cas des mauvaise prédiction de branchement, seules les instructions du ''thread'' fautif doivent être vidées. Pour cela, les ''schedulers'' doivent savoir à quel ''thread'' appartient tel ou telle instruction, pareil pour le ''Reorder Buffer''. Pour cela, on ajoute, dans chaque entrée de ces structures, le ''thread ID'' de l'instruction. Avec le SMT, les différents ''threads'' doivent se partager certaines ressources matérielles : le ''scheduler'', le banc de registres, la ''load store queue'', le cache, l'unité de prédiction de branchement ou d'autres structures matérielles. Le partage de ces ressources entre ''threads'' est dynamique, sauf pour quelques exceptions. La conséquence est qu'il peut y avoir une perte de performance si on lance deux ''threads'' et qu'il n'y a pas assez de ressources matérielles pour en profiter. Les ''threads'' entrent en compétition pour y accéder. Parlons un petit peu du partage du cache et de l'unité de prédiction de branchement. ===Le partage des caches=== La quasi-totalité des architectures multithreadées utilisent un cache partagé, et non des caches dédiés, ce qui impose de gérer le partage des caches entre les différents ''threads''. La première idée est de partitionner le cache en plusieurs morceaux, avec un par ''thread''. Le '''partitionnement statique''' donne des portions égales à chaque programme, ce qui n'est pas toujours optimal mais a l'avantage d'être facile à implémenter. À l'opposé, le '''partitionnement dynamique''' partage le cache selon les besoins des ''thread'' en cours d'exécution. Si un programme a besoin de plus de place que l'autre, il peut réserver un peu plus de place que son concurrent. Mais la gestion du partitionnement est alors plus compliquée, et demande de partitionner efficacement le cache, sans quoi les deux programmes exécutés risquent de se marcher dessus et d'entrer en compétition pour le cache. [[File:Partitionnement de l'Instruction Buffer avec l'hyperthreading.png|centre|vignette|upright=2|Partitionnement des caches avec l'hyperthreading.]] Une autre possibilité est de ne pas partitionner et de laisser les différents ''threads'' accéder au cache comme bon leur semble. Si le cache est physiquement tagué, il n'y a aucune modification à faire. Mais si le cache est virtuellement tagué, une même adresse virtuelle peut correspondre à des adresses physiques différentes pour deux ''threads'' différents. Pour éviter cela, il faut ajouter l'identifiant de ''thread'' dans chaque ligne de cache, dans les bits de contrôle. La détection des succès ou défaut de cache tient compte de l'identifiant de ''thread'' lors des comparaisons de tags, afin d'éviter qu'un ''thread'' lise une donnée d'un autre ''thread''. À l'inverse des processeurs mono-cœur, les processeurs multithreadés préfèrent des caches dont le débit binaire est plus important que la latence mémoire. La raison est que deux ''threads'' consomment deux fois plus de données qu'un seul et le débit binaire du cache doit suivre. Par contre, la latence est moins importante car les cycles gâchés qu'elle créé sont remplis par le ''multithreading'' matériel. Si un défaut de cache prend 4 cycles, le ''multithreading'' matériel va rapidement trouver des instructions à exécuter pendant ces cycles. Tout ce qui vient d'être dit s'applique aussi sur la TLB. Il est possible de la partitionner entre plusieurs ''threads''. La plupart du temps, le partitionnement est statique sur les TLB dédiées aux instructions, alors que les TLB pour les données sont partagées dynamiquement. C'est le cas sur les architectures Skylake d'Intel, où les 128 entrées de la TLB d'instruction de niveau 1 ont découpées en deux sections de 64 entrées, une par programme/''thread'', les autres TLB étant partitionnées dynamiquement. Une autre solution est d'ajouter un identifiant de ''thread'' dans chaque entrée de la TLB, sur le même principe que celui vu dans le chapitre sur la TLB. Le gain en performance est bien plus important, pour un cout en hardware limité. ===L'unité de prédiction de branchement=== Un autre cache est lui aussi partitionné ou complété avec des ''thread ID'' : le ''branch target buffer''. Rappelons que c'est un cache spécialisé pour les adresses de branchements. L'unité de prédiction de branchements doit idéalement séparer les branchements entre chaque ''thread'' pour obtenir de bonnes performances, soit en partitionnant le BTB, soit en ajoutant ajouter un ''Thread ID'' dans chaque entrée de ce cache. Mais ce n'est pas obligatoire. Les unités de prédiction de branchement vues dans les chapitres précédents peuvent parfaitement fonctionner telles quelles sur un processeur multithreadé. Le ''branch target buffer'' mémorise alors des branchements de ''threads'' différents, il ne sait pas à quel ''thread'' appartient tel branchement, mais le tout fonctionne. Le problème est que les prédictions pour un branchement sont parasitées par les branchements d'autres ''threads'', surtout pour les unités se basant sur un historique global. Malgré tout, on obtient un résultat tolérable, les performances sont assez bonnes et cela s'explique avec ce qui suit. Déjà, il arrive que les branchements des autres ''threads'' aient un effet positif sur la prédiction pour un ''thread''. De telles interférences positives sont rares, mais possible si les deux ''threads'' travaillent sur des données partagées, et c'est alors les unités de prédiction de branchement normales, sans partitionnement ni ''thread ID'' qui fonctionnent mieux, sous certaines circonstances. De plus, quand une mauvaise prédiction de branchement est détectée, il faut vider le pipeline des instructions chargées à tort. Et les instructions fautives appartiennent toutes à un même ''thread'', les instructions des autres ''threads'' ne sont pas concernées. Le vidage du pipeline est donc sélectif. Là encore, la vidange du pipeline se base sur les ''Thread ID'' propagés dans le pipeline. Vu que le vidage du pipeline est sélectif, partiel, la perte de performance en cas de mauvaise prédiction de branchement est donc plus faible, et cela surcompense le fait que les prédictions de branchement sont parasitées par les branchements d’autres ''threads''. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Architectures multiprocesseurs et multicœurs | prevText=Architectures multiprocesseurs et multicœurs | next=Les architectures à parallélisme de données | nextText=Les architectures à parallélisme de données }} </noinclude> 4bopb0wi682dbg289wg86w30k3bui97 Fonctionnement d'un ordinateur/L'unité de contrôle 0 69026 773849 765327 2026-10-01T12:25:36Z Mewtow 31375 773849 wikitext text/x-wiki Pour rappel, les instructions se font en plusieurs étapes, appelées micro-opérations. Pour chaque instruction, il faut déduire quelles sont les micro-opérations à exécuter et dans quel ordre. Mais l'instruction chargée depuis la mémoire ne précise pas les micro-opérations à faire, elle se contente juste de dire quelle opération effectuer et sur quels opérandes. Le processeur doit donc traduire l'instruction en une séquence de micro-opérations, en une séquence de signaux de commandes adéquats. C'est le rôle de l''''unité de décodage d'instruction''', une portion du processeur qui « décode » l'instruction. [[File:Unité de décodage d'instruction.png|centre|vignette|upright=1.5|Unité de décodage d'instruction]] Une micro-opération configure le chemin de donnée d'une manière bien précise, afin de faire une opération de base : copie entre registres, accès mémoire, opération sur l'ALU. Pour cela, il faut configurer l'ALU pour qu'elle fasse l'opération adéquate, configurer le banc de registre pour lire /écrire les bons registres, etc. La micro-opération envoie des '''signaux de commande''' adéquats au chemin de données. Pour simplifier, une micro-opération est encodée en concaténant les signaux de commande pour l'ALU, ceux pour les registres, pour l'unité mémoire, etc. Chaque micro-opération encode les signaux de commande à destination du chemin de données. {|class="wikitable" |- ! colspan="4" | Micro-opération, encodage en binaire |- | Signaux de commande pour l'ALU | Signaux de commande pour les registres | Signaux de commande pour l'unité d'accès mémoire | Autres signaux de commande |} Il existe des processeurs assez rares où chaque instruction machine est une micro-opération. Son encodage précise directement les signaux de commande, pas besoin d'une unité de décodage d'instruction. De telles architectures sont appelées des ''architectures actionnées par déplacement''. Elles feront l'objet d'un chapitre dédié, nous allons les mettre de côté pour le moment et nous concentrer sur des architectures plus courantes. ==Les séquenceurs câblés et microcodés== Pour un même jeu d'instruction, des processeurs de marque différente peuvent avoir des séquenceurs différents. Les différences entre séquenceurs sont nombreuses, une partie étant liée à des optimisations plus ou moins sophistiquées du décodage. Mais l'une d'entre elle permet de distinguer deux types purs de séquenceurs, sur un critère assez pertinent. La distinction se fait sur la nature du séquenceur, sur le circuit de décodage utilisé. Le séquenceur est un circuit séquentiel, c’est-à-dire qu'il contient un circuit combinatoire et des registres. Or, nous avons vu dans les chapitres précédents que tout circuit combinatoire peut être remplacé ainsi par une ROM avec le contenu adéquat. Et le circuit combinatoire dans le séquenceur ne fait pas exception à cette règle. Le circuit combinatoire peut être implémenté de trois grandes manières différentes. * La première méthode est d'utiliser un circuit combinatoire proprement dit, construit avec des portes logiques, en utilisant les méthodes du chapitre sur les portes logiques. * La seconde remplace ce circuit par une mémoire ROM dans laquelle on écrit la table de vérité du circuit. * La troisième solution est une solution intermédiaire qui utilise un circuit dit PLA (''Programmable Logic Array''). Il y a donc un choix à faire : est-ce le séquenceur incorpore un circuit combinatoire ou une mémoire ROM ? Cela permet de distinguer les séquenceurs câblés, basés sur un circuit combinatoire/séquentiel, et les séquenceurs microcodés, basés sur une mémoire ROM. Les deux ont évidemment des avantages et des inconvénients différents, comme nous allons le voir. ==Les séquenceurs câblés== Si les instructions sont décodées par un assemblage de portes logiques et de registres, on parle de '''séquenceur câblé'''. Plus le nombre d'instructions est important, plus un séquenceur câblé est compliqué à concevoir par rapport à ses alternatives. La complexité du séquenceur dépend aussi de la complexité des instructions machine. Autant dire que les processeurs CISC n'utilisent pas trop ce genre de séquenceurs et préfèrent utiliser des séquenceurs microcodés ou hybrides, alors que les séquenceurs câblés sont préférés sur les processeurs RISC. ===L'implémentation du séquenceur=== Sur certains processeurs assez rares, toute instruction s’exécute en une seule micro-opération, ce qui fait que le séquenceur se résume alors à un simple circuit combinatoire. C'est très rare, car cela implique que toutes les instructions doivent se faire en moins d'un cycle d'horloge. Pour cela, la durée d'un cycle d'horloge doit se caler sur l'instruction la plus lente : un accès mémoire prendra autant de temps qu'une addition, ou qu'une multiplication, etc. Ensuite, il faut que le processeur soit une architecture Harvard, afin de charge l'instruction tout en accédant aux données en parallèle, le tout en un seul cycle d'horloge processeur. [[File:Séquenceur combinatoire 01.png|centre|vignette|upright=2.5|Séquenceur combinatoire]] Sur les autres processeurs, il y a des instructions qui demandent d’exécuter une suite de micro-opérations. Pour cela, le séquenceur devient un circuit séquentiel, qui intègre un registre/compteur. La présence de ce registre s’explique par le fait que le séquenceur a besoin de savoir à quelle micro-opération il en est, information qui est mémorisée dans un registre. [[File:Séquenceur séquentiel.png|centre|vignette|upright=2|Séquenceur séquentiel]] Dans le cas le plus simple, le séquenceur est basé sur un simple compteur couplé à un circuit combinatoire. Le compteur mémorise à quelle micro-opération il en est, en lui attribuant un numéro : s'il en est à la première, seconde, troisième micro-opération, etc. Le compteur est incrémenté à chaque micro-opération réussie (les accès mémoires peuvent prendre plusieurs cycles pour une seule micro-opération, si le CPU doit attendre la RAM). Il est réinitialisé quand l'instruction se termine, à savoir quand le compteur a atteint le nombre de micro-opérations adéquat pour exécuter l'instruction. Le compteur n'est pas forcément un compteur normal, qui stocke une valeur en binaire. Il s'agit souvent d'un compteur basé un registre à décalage, appelé un '''compteur ''one-hot''''', ou encore un compteur en anneau. La raison est que les compteurs en anneau sont très rapides et utilisent peu de circuits, sans compter qu'ils permettent de se passer de comparateur pour déterminer la valeur du compteur. Leur seul défaut est que les économies en portes logiques sont contrebalancées par un plus grand nombre de bascules, qui est cependant acceptable si le compteur encode peu de valeurs. Si on veut un séquenceur qui fonctionne rapidement, en moins d'un cycle d'horloge, c'est la meilleure solution qui soit. En combinant le compteur avec l'opcode, le séquenceur détermine quel est la micro-opération à effectuer. Pour être plus précis, un circuit combinatoire intégré au séquenceur prend en entrée le compteur et l'opcode de l'instruction machine, puis fournit en sortie la micro-opération adéquate. Dans son implémentation la plus simple, ce circuit combinatoire est composé de deux sous-circuits : un décodeur et une "matrice" de portes logiques. Le décodeur prend en entrée l'opcode et a une sortie pour chaque instruction possible, ce qui fait qu'on l'appelle le '''décodeur d'instruction'''. La matrice de portes prend en entrée les sorties du décodeur et le compteur, et sort les signaux de commande adéquats. Pour chaque instruction et chaque valeur de compteur, elle sort les signaux de commande correspondant à la micro-opération adéquate. Un exemple est illustré ci-dessous. L'exemple est celui de l'exécution d'une instruction qui charge une donnée dans le registre dit accumulateur d'un processeur à accumulateur (qui n'a qu'un seul registre, le dit accumulateur). Le tout se fait en 6 cycles, dont 4 servent à gérer le chargement de l'instruction et le ''program counter''. * Le premier cycle copie le ''program counter'' dans le registre d’interfaçage pour les adresses. * Le second cycle lance une lecture, la donnée lue est sur le bus de données à la fin du cycle. * Le troisième copie l'instruction lue dans le registre d’interfaçage pour les données et dans le registre d'instruction, et incrémente le ''program counter'' en parallèle. * Le quatrième copie l'adresse à lire dans le registre d’interfaçage d'adresse. * Le cinquième lit la donnée à lire depuis la mémoire. * Le sixième copie la donnée lue du registre d’interfaçage dans l'accumulateur. [[File:Animation of an LDA instruction performed by the control matrix of a simple hardwired control unit.gif|centre|vignette|upright=2.5|Implémentation de la matrice de portes d'un séquenceur câblé. Les sorties du décodeur sont à gauche, le compteur (''one hot'') est en haut, les signaux de commandes sont émis vers le bas.]] Pour résumer, un séquenceur câblé est composé d'un compteur de micro-opération, d'un décodeur d'instruction et d'une matrice de portes logiques. Dans le schéma précédent, vous voyez que l'usage d'un compteur ''one hot'' facilite l'implémentation de la matrice de portes logiques. ===La détermination de la fin d'une instruction=== Notons que le compteur interne au séquenceur est aussi utilisé pour déterminer quand une instruction se termine. Quand une instruction se termine, le processeur doit faire deux choses : réinitialiser le compteur du séquenceur, et surtout : incrémenter le ''program counter'' pour passer à l'instruction suivante. Pour cela, on ajoute un circuit combinatoire qui détermine si l'instruction en cours est terminée. Une instruction se termine quand la dernière micro-opération est atteinte, à savoir qu'une instruction qui se termine à la énième micro-opération se termine quand le compteur atteint N. Par exemple, pour une instruction de multiplication de 6 cycles d'horloge, le décodeur sait que l'instruction est terminée le compteur atteint 5 (signe qu'il en est à sa sixième micro-opération, soit la dernière). Le circuit combinatoire qui détermine si l'instruction est terminée est donc trivial : il associe une table qui attribue pour chaque opcode le numéro de la dernière micro-opération, et un comparateur qui vérifier si le compteur a atteint cette valeur. Une manière de faire plus simple est d'utiliser un décompteur, qui est décrémenté à chaque micro-opération exécutée, et de l'initialiser avec le nombre de micro-opérations de l'instruction exécutée. L’instruction est alors terminée quand le compteur atteint zéro. Ce faisant, le circuit qui détecte la fin d'une instruction est terriblement simple, sans compter qu'il gère naturellement le cas où les instructions n'ont qu'une seule micro-opération. Mais cela n'élimine pas le circuit qui détermine le nombre de cycles d'une instruction, car celui-ci sert pour initialiser le compteur. Cette solution n'est pas toujours utilisée, pour des raisons assez diverses, notamment le fait qu'elle se marie assez mal avec diverses techniques d'optimisation. Les deux techniques précédentes fonctionnent bien à condition qu'une instruction machine corresponde toujours à la même séquence de micro-opérations. Mais ce n'est pas toujours le cas et la séquence exacte peut différer selon l'état du processeur. Le cas classique est celui des accès mémoires, où le processeur doit attendre que la donnée demandée soit lue ou écrite. Comme autre exemple, certaines étapes/micro-opérations peuvent être facultatives et ne s’exécuter que sous certaines conditions. Pensez par exemple au cas des instructions à prédicats ou des branchements. Mais on peut avoir la même chose avec des instructions de multiplication ou de division, pour lesquelles le calcul peut être plus rapide avec certains opérandes. Dans ce cas, le compteur doit pouvoir sauter certaines micro-opérations et passer par exemple de la deuxième micro-opération à la dixième directement. Et cela demande d'ajouter quelques circuits combinatoires pour cela. Par exemple, le décodeur peut incorporer une sortie pour préciser le numéro de la micro-opération suivante, ce numéro servant à réinitialiser le registre du compteur. Le séquenceur prend en entrée le compteur, l'opcode de l'instruction, éventuellement d'autres entrées, et fournit en sortie : les signaux de commande, et le prochain état du compteur. Ou alors, le décodeur d'instruction dit de combien il faut sauter de micro-opération, de combien il faut augmenter le compteur. ==Les séquenceurs microcodés== Pour limiter la complexité du séquenceur, les concepteurs de processeurs ont inventé les '''''séquenceurs microcodés'''''. L'idée derrière ces séquenceurs microcodés est que, pour chaque instruction, la suite de micro-opérations à exécuter est pré-calculée et mémorisée dans une mémoire ROM, au lieu d'être déterminée à l’exécution par un circuit combinatoire. La mémoire ROM qui stocke la suite de micro-opérations équivalente pour chaque instruction microcodée s'appelle le '''''control store''''', tandis que son contenu s'appelle le '''microcode'''. : Par abus de langage, nous parlerons parfois de microcode pour désigner la suite de microinstructions correspondant à une instruction machine. Nous parlerons alors de microcode de l'addition pour désigner la suite de microinstructions correspondant à l'instruction machine de l'addition. Faire cette petite erreur rendra la lecture de cette section beaucoup plus fluide. Les séquenceurs microcodés étaient surtout utilisés sur les architectures CISC, celles avec un jeu d'instruction étoffé et beaucoup de modes d'adressages différents. Leur grand nombre d'instructions favorisait un microcode. De plus, le budget en transistor de ces processeur était assez limité, ce qui fait que ces opérations aujourd'hui banales n'avaient pas leur propre circuit et étaient émulées en microcode. Les premiers microprocesseurs 16 bits utilisaient souvent le microcode pour implémenter des instructions comme la multiplication et la division. Un exemple est le 8086 d'Intel, qui n'avait pas de circuit multiplieur/diviseur. À la place, il émulait la multiplication avec une série d'additions et de décalages, et la division avec des soustractions/décalages. Les processeurs de ce type utilisaient un microcode pour beaucoup d'instructions, pas seulement la multiplication et la division. En conséquence, ajouter des instructions dans un microcode "existant" coutait moins cher que d'ajouter un circuit multiplieur. Un autre exemple d'utilisation du microcode est celui des premiers processeurs capables d'effectuer des calculs flottants. Sur les premiers processeurs de ce type, il n'y avait pas de FPU, pas de circuits pour les calculs flottants. Les instructions flottantes étaient en réalité émulées par des calculs entiers : chaque instruction flottante était convertie en interne en une suite d'instructions entières qui émulaient l'instruction voulue. Pour cela, les instructions flottantes étaient microcodées. De nos jours, les processeurs contiennent des circuits de calcul flottant, ce qui fait que les instructions ne sont plus émulées sauf pour quelques-unes. Les séquenceurs micro-codés sont plus simples à concevoir et simplifient beaucoup le travail des concepteurs de processeurs. L'usage du microcode permet aussi d'ajouter des instructions facilement, en modifiant le microcode, sans pour autant modifier en profondeur le processeur. En contrepartie, un séquenceur microcodé utilise plus de portes logiques, vu qu'une ROM est un circuit gourmand en portes logique. En théorie, les instructions microcodées peuvent être plus rapides que leur équivalent logiciel, à savoir une instruction émulée par une suite d'instructions machines. Le microcode peut être optimisé de manière à mieux utiliser les ressources internes au processeur. Mais force est de constater que ces opportunités d’optimisation étaient rares dans la réalité. Mais cela n'était pas l'intérêt principal, car les architectures CISC qui privilégiaient la taille du programme - la ''code size''. L'usage d'un microcode n’a plus trop d'intérêt de nos jours, et surtout pas sur les architectures RISC qui se contentent d'un séquenceur câblé. ===Le ''control store''=== La caractéristique principale du ''control store'' est sa capacité, qui est souvent assez petite. La capacité du ''control store'' dépend non seulement du nombre de micro-instructions qu'il contient, mais aussi de la taille de ces dernières. Un byte du ''control store'' correspond à une micro-instruction, les exceptions étant très très rares. Et la taille des micro-instructions varie grandement d'un processeur à l'autre. Dans les grandes lignes, la différence principale tient beaucoup la manière dont sont encodées les micro-instructions. Il existe plusieurs sous-types de séquenceurs microcodés, qui se distinguent par la façon dont sont codées les micro-opérations. * Avec le '''microcode horizontal''', chaque instruction du microcode encode directement les signaux de commande à envoyer aux unités de calcul. Vu Le grand nombre de signaux de commande, il n'est pas rare que les micro-opérations d'un microcode horizontal fassent plus d'une centaine de bits ! * Avec un '''microcode vertical''', les instructions du microcode sont traduites en signaux de commande par un séquenceur câblé qui suit le ''control store''. Son avantage est que les micro-opérations sont plus compactes, elles font moins de bits. Cela permet d'utiliser un ''control store'' plus petit ou d'avoir un microcode plus important, au détriment de la complexité du séquenceur. Un exemple de microcode vertical est le microcode du 8086, encore lui ! Pour ce qui est de la commande de l'ALU, le microcode envoie une commande abstraite qui est décodée par un circuit combinatoire (un PLA), pour obtenir les signaux de commande de l'ALU. L'implémentation interne du ''control store'' ne suit pas forcément à la lettre l'organisation en byte. Pour faire comprendre ce que je veux dire, prenons l'exemple de l'Intel 8086, dont le ''control store'' contenait 512 bytes/microinstructions de 21 bits chacune. Le ''control store'' n'était pas une ROM de 512 lignes et de 21 colonnes, comme on pourrait s'y attendre. Les dimensions 512 par 21 donneraient une ROM très allongée, rendant son placement sur la puce de silicium peu pratique. À la place, elle regroupait 4 bytes par ligne, ce qui donnait 84 lignes et 128 colonnes. ===L'optimisation du microcode=== Le ''control store'' a souvent une capacité très faible, même pour une mémoire ROM. Une ROM prend de la place, ce qui fait que les concepteurs de processeurs préfèrent utiliser une ROM assez petite. Néanmoins, malgré la petitesse des ROM de l'époque, il arrivait souvent que le ''control store'' contienne des vides, des bytes inoccupés. Cela arrive si le microcode n'a pas une taille égale à une puissance de deux. Par exemple, si l'on a un microcode qui occupe 120 bytes, on doit utiliser un ''control store'' de 128 bytes, ce qui laisse 8 bytes vides. On pourrait croire que les vides sont placés à la fin du ''control store'', mais il est parfois préférable de disperser les vides dans le ''control store'', afin de simplifier les circuits adossés au microcode, ce que nous allons voir dans ce qui suit. Pour les concepteurs de processeurs, une difficulté majeure est de faire rentrer le microcode dans le ''control store''. C'est encore un problème à l'heure actuelle, mais ce l'était encore plus sur les architectures anciennes, qui devaient faire avec des ROM limitées qu'actuellement. De plus, sur les anciennes architectures CISC, le grand nombre d'instructions recherchait se mariait mal à la petite capacité des mémoires ROM de l'époque. Les concepteurs de processeurs devaient ruser pour faire rentrer un microcode souvent complexe dans une petite ROM. Diverses optimisations étaient possibles. La première optimisation de ce genre consiste à gérer des fonctions/sous-programmes/routines logicielles dans le microcode. Pour cela, les circuits en charge du microcode géraient l’exécution de fonctions dans le microcode, avec des registres pour l'appel de retour, des microinstructions pour faire des branchements dans le microcode et tout ce qui va avec. Mais le tout était généralement simplifié et rares étaient les processeurs qui incorporaient une pile d'appel complète pour le microcode. Beaucoup se limitaient à ajouter un registre pour l'adresse de retour, quelques instructions de branchement interne au microcode, et guère plus. Un exemple assez intéressant est celui du processeur Intel 8086, dont le microcode contient une sous-routine pour gérer chaque mode d'adressage. Sans optimisations, il faudrait un microcode par instruction et par mode d'adressage. Par exemple, le microcode pour une addition en mode d'adressage immédiat n'est pas la même que pour une instruction d'addition en mode d'adressage direct. Cependant, elles partagent un même cœur qui s'occupe de l'addition et de la gestion de l'accumulateur, même si la gestion des opérandes est totalement différente suivant le mode d'adressage. Pour éliminer cette redondance, le microcode du 8086 délègue la gestion des modes d'adressages et des opérandes à des sous-programmes spécialisés, une par mode d'adressage. La seconde optimisation est de réduire la taille des micro-instructions en jouant sur leur encodage. L'usage d'un microcode vertical est une première solution. Décoder certaines instructions simples sans passer par le microcode en est une autre, et elle donne les séquenceurs hybrides dont nous parlerons dans la suite du chapitre. Mais d'autres techniques sont possibles, comme le fait de déporter une partie du décodage en-dehors du ''control store'', dans des circuits logiques séparés. Un bon exemple de cela est celui de l'Intel 8086, encore lui, sur lequel beaucoup d'instructions existaient en deux exemplaires : une version 8 bits et une version 16 bits. Il n'y avait pas de microcode séparé pour les deux versions, mais un seul microcode qui s'occupait autant de la version 8 bits que de la version 16 bits de l'instruction. La différence entre les deux se faisait au niveau du bus interne du processeur. Un bit de l'instruction machine indiquait s'il s'agissait d'une version 8 ou 16 bits et ce bit était transmis à la machinerie du bus interne, sans passer par le microcode. ===Les circuits d’exécution du microcode=== Le processeur doit trouver un moyen de dérouler les micro-instructions les unes après les autres, ce qui est la même chose qu'avec des instructions machines. Le micro-code est donc couplé à un circuit qui de l’exécution des micro-opérations les unes après les autres, dans l'ordre. Ce circuit est l'équivalent du circuit de chargement, mais pour les micro-opérations. Pour cela, il y a deux méthodes, que voici. La première méthode fait que chaque micro-instruction contient l'adresse de la micro-instruction suivante. Avec cette méthode, on peut disperser une suite de microinstructions dans le ''control store'', au lieu de garder des microinstructions consécutives. L'utilité de cette méthode n'est pas évidente, mais elle deviendra plus claire dans la section suivante. [[File:Microcode sans microséquenceur.gif|centre|vignette|upright=1.5|Microcode sans microséquenceur.]] La seconde méthode fait que le séquenceur contient un équivalent du ''program counter'' pour le microcode. On trouve ainsi un '''micro-séquenceur''' qui regroupe un '''registre d’adresse de micro-opération''' et un '''micro-compteur ordinal'''. Le registre d’adresse de micro-opération est initialisé avec l'opcode de l'instruction à exécuter, qui pointe vers la première micro-instruction. Le micro-compteur ordinal se charge d'incrémenter ce registre à chaque fois qu'une micro-instruction est exécutée, afin de pointer sur la suivante. [[File:Microcode avec un microséquenceur.gif|centre|vignette|upright=2|Microcode avec un microséquenceur.]] Un séquenceur microcodé peut même gérer des micro-instructions de branchement, qui précisent la prochaine micro-instruction à exécuter. Grâce à cela, on peut faire des boucles de micro-opérations, par exemple. Pour gérer les micro-branchements, il faut rajouter la destination d'un éventuel branchement dans les micro-instructions de branchement. La taille des micro-instructions augmente alors, vu que toutes les micro-opérations ont la même taille. Voici ce que cela donne pour les microcodes avec un microcompteur ordinal. On voit que l'ajout des branchements modifie le microcompteur ordinal de façon à permettre les branchements entre micro-opérations, d'une manière identique à celle vue pour l'unité de chargement. [[File:Branchements avec microcode horizontal avec microséquenceur.gif|centre|vignette|upright=2|Branchements avec microcode horizontal avec microséquenceur.]] Voici ce que cela donne pour les microcodes où chaque micro-instruction contient l'adresse de la suivante : [[File:Branchements avec microcode horizontal sans microséquenceur.gif|centre|vignette|upright=2|Branchements avec microcode horizontal sans microséquenceur.]] Il est possible de créer des fonctions/sous-programmes/sous-routines dans le microcode, grâce à ces micro-branchements et en ajoutant un registre pour gérer l'adresse de retour. ===Localiser la première microinstruction à exécuter dans le ''control store''=== Un premier problème à résoudre avec un microcode, est de localiser la suite de micro-instructions à exécuter. Si l'on veut exécuter une instruction machine, le microcode doit trouver le début de la suite de microinstruction dans le microcode et démarrer l’exécution des microinstructions à partir de là. Pour le dire autrement, le séquenceur doit déterminer, à partir de l'opcode, quelle est l'adresse de départ dans le ''control store''. Pour cela, il y a plusieurs solutions. La première solution fait une traduction de l'opcode vers l'adresse de départ, en utilisant un circuit combinatoire et/ou une mémoire ROM. Elle a l'inconvénient de complexifier le processeur, dans le sens où on doit ajouter des circuits en plus. De plus, le circuit ou la ROM ajoutés mettent un certain temps avant de donner leur résultat, ce qui ralentit quelque peu le décodage des instructions. L'avantage principal est que l'on peut utiliser facilement un microséquenceur basique et placer les microinstructions les unes à la suite des autres dans le ''control store''. Cette technique s'utilise aussi bien avec un micro-séquenceur que sans. Dans les faits, elle s'utilise de préférence avec un micro-compteur ordinal. L'usage de ce dernier réduit fortement la taille du ''control store'', ce qui compense le fait de devoir ajouter des circuits pour faire la traduction opcode -> adresse. [[File:Control store adressé par predecodage de l'opcode.png|centre|vignette|upright=2|Control store adressé par predecodage de l'opcode]] L'autre solution considère l'opcode de l'instruction microcodée comme une adresse : le ''control store'' est conçu pour que cette adresse pointe directement sur le début de la suite de micro-opérations correspondante, la première micro-instruction de cette suite. Du moins, c'est le principe général, mais un détail vient mettre son grain de sel : un ''control store'' utilise systématiquement des adresses plus grandes que l'opcode. Ce qui fait qu'il faut rajouter des bits à l'opcode pour obtenir l'adresse, on doit concaténer des zéros à l'opcode pour obtenir l'adresse finale. On fait alors face à deux choix : soit on met l'opcode dans les bits de poids faible de l'adresse, soit on la place dans les bits de poids fort. Les deux solutions ont des avantages et inconvénients différents. [[File:Control store.gif|centre|vignette|upright=2|Control store d'un microcode horizontal.]] La première méthode place les opcodes dans les bits de poids faible et les zéros dans les bits de poids fort. Le défaut principal de cette méthode vient du fait que de nombreux opcodes ont des représentations binaires proches, ce qui fait que leurs adresses de départs sont proches dans le ''control store''. Il n'y a alors pas assez d'espace entre les deux adresses de départ pour y placer une suite de microninstructions. En clair, cette méthode ne peut pas s'utiliser avec un micro-séquenceur. Par contre, elle se marie très bien avec un ''control store'' où chaque microinstruction contient l'adresse de la suivante. En faisant cela, l'opcode pointe vers l'adresse de départ, mais le reste de la suite de microinstructions est placé ailleurs dans le ''control store'', dans des adresses qui ne correspondent pas à des opcodes. Les adresses de départ occupent donc le bas de la ROM du ''control store'', alors que le haut de la ROM contient les suites de microinstructions et éventuellement des vides. [[File:Control store adressé par l'opcode - opcode sur bits de poids faible.png|centre|vignette|upright=2|Control store adressé par l'opcode - opcode sur bits de poids faible]] La seconde méthode met l'opcode dans les bits de poids fort de l'adresse et les zéros dans les bits de poids faible. En faisant cela, les adresses de départ sont dispersées dans le ''control store'', elles sont séparées par des intervalles de taille de fixe. Cela garantit qu'il y a un espace fixe entre deux adresses de départ, dans lequel on peut placer une suite de microinstructions. Un bon exemple est celui du 8086, dont le microcode, très complexe, espace chaque instruction/opcode tous les 16 bytes, ce qui permet d'avoir 16 microinstructions par instruction machine. Son ''control store'' contenait 512 micro-instructions, 512 bytes, ce qui donne des adresses de 13 bits. Mais l'opcode occupait les 9 bits de poids fort de l'adresse de microcode, ce qui laissait 4 bits de poids faible libres. En conséquence, chaque instruction machine disposait de maximum 16 microinstructions consécutives. L'avantage de cette méthode est que l'on peut utiliser un microséquenceur plus petit, avec un incrémenteur de plus petite taille. De plus, les adresses utilisées pour les branchements dans le microcode sont plus petites. Par exemple, le microcode du 8086, qui espacait ses microinstructions toutes les 16 bytes, avait un microséquenceur de 4 bits. Ce dernier contenait un incrémenteur de micro-''program counter'' de 4 bits et non 13. De plus, les adresses utilisées pour les branchements dans le microcode ne faisaient que 4 bits, à savoir qu'il s'agissait de branchements relatifs. Tout cela rendait le microséquenceur beaucoup plus économe en circuits. Cette solution a cependant pour défaut de laisser beaucoup de vides dans le ''control store''. Le microcode de certaines instructions était assez court, d'autres avaient un microcode plus long. L'espace entre deux opcodes, entre deux adresses de départ, est fixe et se cale sur le microcode le plus long. En conséquence, le microcode de certaines instructions laisse des vides à sa suite. Si on sépare les adresses de départ par un espace assez court, alors les suites d'instructions trop longues ne rentrent pas, sauf en trichant. Par tricher, on veut dire que le microcode de ces instruction est découpé en morceaux et dispersé dans les vides du ''control store''. L’exécution d'un microcode dispersé ainsi se fait normalement grâce aux microinstructions de branchement. [[File:Control store adressé par l'opcode - opcode sur bits de poids fort 01.png|centre|vignette|upright=2|Control store adressé par l'opcode - opcode sur bits de poids fort]] Pour comparer les trois méthodes, on peut comparer ce qu'il en est pour le remplissage du ''control store''. Les deux premières méthodes remplissent le ''control store'' au mieux, alors que la dernière laisse des vides et disperse les suites de microinstructions dans le ''control store''. Par contre, il faut aussi tenir compte d'autres paramètres. La première solution demande d'ajouter des circuits de traduction opcode -> adresse qui prennent de la place, pas les deux dernières solutions. Enfin, la deuxième solution impose de rallonger les bytes du ''control store'', car on se prive de micro-séquenceur, ce qui n'est pas le cas des deux autres. Au final, comparer les trois solutions ne donne pas de gagnant absolu : tout dépend de l'implémentation du jeu d'instruction choisit, de son encodage, etc. ===La mise à jour du microcode=== Parfois, le processeur permet une mise à jour du ''control store'', ce qui permet de modifier le microcode pour corriger des bugs ou ajouter des instructions. L'utilisation principale est de corriger des bugs ou des problèmes de sécurité assez tordus. Il est fréquent que les processeurs aient des bugs matériels, présents à cause de défauts de conception parfois subtils. Les grands fabricants comme Intel et AMD documentent ces bugs dans leur documentation officielle. Une petite partie de ces bugs peuvent se corriger avec une mise à jour du microcode, et ils ne sont pas forcément dans le microcode lui-même. Un exemple serait la désactivation des instructions TSX sur les processeurs x86 Haswell, en 2014, qui ont été désactivées par une mise à jour du microcode, après qu'un bug de sécurité ait été découvert. La mise à jour du microcode est rarement permanente. Une mise à jour permanente du microcode implique que le ''control store'' est une EEPROM ou une mémoire ROM reprogrammable, donc des mémoire très difficiles à mettre en œuvre dans les processeurs. Or, le ''control store'' doit être une mémoire extrêmement performante, capable de fonctionner à très haute fréquence, avec des temps d'accès minuscules, aux performances proches d'une SRAM. En réalité, le ''control store'' est mis à jour temporairement, et est réinitialisé à chaque boot de l'ordinateur, à chaque boot du processeur. Pour cela, le ''control store'' est implémenté avec deux mémoires : une ROM qui contient le microcode originel, et une SRAM. Pour simplifier les explications, nous allons appeler ces deux mémoires la micro-ROM et la micro-RAM. Au démarrage de l'ordinateur, le microcode contenu dans la micro-ROM est copié dans la micro-RAM. Peu après l'allumage du processeur, le contenu de la micro-RAM peut être remplacé par un microcode mis à jours. Typiquement, le microcode corrigé est fourni soit par le BIOS, soit par le système d'exploitation. Les mises à jour de microcode sont généralement soumises à des mesures de sécurité drastiques intégrées au processeur. Par exemple, le microcode fournit par le fabricant est chiffré avec des clés connues seulement des fabricants de CPU, autres), et un microcode n'est chargé par le processeur que si la clé correspond. ===Les microcodes réinscriptibles=== Il existe des processeurs dont le microcode est directement reprogrammable, accessible par le programmeur. Le programmeur peut écrire ce qu'il veut dans la micro-RAM, à sa guise. On peut ainsi changer le jeu d'instruction du processeur au besoin, afin d'ajouter des instructions utiles. Il s'agit de processeurs destinés à l'embarqué, qui doivent être conçus sur mesure, on ne trouve pas de systèmes de ce genre dans les PCs. Ils sont appelés des '''processeurs à microcode réinscriptible'''. L'utilité est que les programmes peuvent disposer des instructions les plus adéquates pour leur fonction, ce qui réduit la taille du code (la mémoire prise par le programme exécutable) et facilite la programmation en assembleur. Ces deux avantages n'ont pas grand intérêt de nos jours. De plus, l'utilisation de cette technique demande un ''control store'' assez imposant, de grande taille, rarement rapide. Par contre, cette fonctionnalité a de nombreux défauts. Si chaque programme peut changer à la volée le jeu d'instruction du processeur, cela peut mettre le bazar. Si un programme change le microcode, les programmes qui passent après lui doivent réinitialiser le microcode pour ne pas exécuter des instructions incorrectes. Cela peut aussi poser des problèmes de sécurité, les hackers étant doués pour utiliser ce genre de fonctionnalités à des fins malveillantes. Cependant, les processeurs de ce type sont utilisés pour l'embarqué, sur des systèmes où un seul programme s'exécute, sans accès à un réseau local, il n'y a donc pas de problèmes liés à des programmes malveillants ou des interactions entre programmes. Un processeur de ce type est le GP1000 d'Imsys, qui a été décrit entres autres dans l'article "GP1000 Has Rewritable Microcode" du magazine ''Microprocessor report''. Il disposait d'une micro-ROM de 36 kibioctets, couplées à une micro-RAM de 18 kibioctets. Une partie du microcode était donc réinscriptible : un tiers l'était, les deux autres tiers étaient dans une micro-ROM impossible à modifier. Il y avait donc un microcode permanent en micro-ROM et un microcode variable en en micro-RAM. Le microcode permanent contient un ''chargeur de microcode'', qui recopie le microcode variable dans la micro-RAM, à l'allumage de l'ordinateur. Par la suite, le microcode variable est chargé avec une instruction machine dédiée. Un point très original est que le GP1000 supporte l'exécution de plusieurs programmes à tour de rôle. Le microcode permanent contient de quoi gérer des interruptions, émuler des périphériques virtuels, mais aussi gérer la présence de plusieurs programmes en cours d'exécution. Les programmes s'exécutent à tour de rôle et le microcode décide qui s’exécute et pour combien de temps. Le microcode permanent contient donc une sorte de mini-système d'exploitation rudimentaire ! Au-delà ce ça, Imsys fournissait des microcodes près à l'emploi, qui pouvaient être chargés à la demande. L'un d'entre eux permettait d'implémenter la machine virtuelle Java directement dans le processeur. Pour information, la machine virtuelle Java standardise le langage machine d'un processeur fictif, précisément une machine à pile simple, qui peut en théorie être implémentée en matériel. C'est exactement ce que faisait l'un des microcodes près à l'emploi d'Imsys. ==Les séquenceurs hybrides== Les séquenceurs hybrides sont un compromis entre séquenceurs câblés et microcodés. Ils permettent de profiter des avantages et inconvénients des deux types de séquenceurs. Sur le principe, une partie des instructions est décodée par une partie câblée, et l'autre passe par le microcode. Typiquement, de tels séquenceurs sont très fréquents sur les architectures CISC, où ils permettent un décodage rapide pour les instructions simples, alors que les instructions complexes le sont par le microcode, plus lent. L'organisation interne d'un séquenceur hybride varie grandement selon le processeur et le jeu d'instruction. Dans le cas le plus simple, on a un séquenceur câblé secondé par un séquenceur microcodé, les deux étant précédés par un '''circuit de prédécodage'''. Le circuit de prédécodage reçoit les instructions et les redirige soit vers le séquenceur câblé, soit vers le séquenceur microcodé. Les instructions les plus simples sont dirigées vers le séquenceur câblé, alors que les instructions complexes vont vers le microcode (généralement les instructions avec des modes d'adressage exotiques). Une solution intéressante est de décoder les instructions qui prennent un seul cycle dans un séquenceur câblé, alors que les instructions multicycles sont décodées par un séquenceur microcodé séparé. Mais dans le cas général, la séparation en deux séquenceurs n'est pas évidente et on trouve un ''control store'' entouré de circuits câblés, avec certaines instructions qui n'ont pas besoin du microcode pour être décodées, d'autres qui passent par le microcode, d'autres qui sont décodé partiellement par microcode et partiellement par des circuits câblés. Notons que le microcode vertical n'est pas un séquenceur hybride, car toutes les instructions passent par le microcode. Par contre, un séquenceur hybride peut utiliser un microcode vertical, ce qui rend le séquenceur assez compliqué. Sur les processeurs x86 modernes, on trouve plusieurs séquenceurs : plusieurs décodeurs câblés spécialisés, et un microcode séparé. ===Le séquenceur hybride du 8086=== Un bon exemple de séquenceur de ce type est celui du processeur x86 8086 d'Intel, ainsi que ceux qui ont suivi. Le jeu d'instruction x86 est tellement complexe qu'il utilise un séquenceur hybride. Le séquenceur de l'Intel 8086 est organisé comme suit : un ''control store'' de 512 microinstructions (512 bytes) couplé à de nombreux circuit câblés, et une ''Group Decode ROM'' qui décide pour chaque instruction si elle est décodée par le séquenceur câblé ou le microcodé. La mal-nommée ''Group Decode ROM'' est en réalité un petit circuit combinatoire un peu particulier (basé sur un PAL, composant proche d'une ROM), qui commande le séquenceur proprement dit. Il fournit 15 signaux qui configurent le séquenceur et disent si le décodage doit utiliser ou non le microcode. Il permet aussi de configurer le microcode pour gérer les différents modes d'adressage, ou encore de configurer les circuits câblés en aval du microcode. Sur ce processeur, les instructions qui s’exécutent en un seul cycle d'horloge sont décodés sans utiliser le microcode. Le 8086 utilise une sorte de micro-code vertical pour commander l'ALU. Entre l'ALU et le microcode, on trouve un mini-décodeur, qui décode l'opération envoyée par le microcode en signaux de commande. C'est un simple circuit combinatoire (un PLA pour être précis). La raison est que l'ALU du 8086 est quelque peu complexe. Nous l'avions vu dans le chapitre sur les unités de calcul, l'ALU du 8086 est basée sur un additionneur à propagation de retenue, où deux portes logiques sont remplacées par une porte logique universelle. Il faut commander ces deux portes universelles pour obtenir l'opération voulue, ce qui demande pas mal de signaux de commande. Et pour économiser de la place dans le microcode, l'opération à faire est encodée sur plusieurs bits, qui sont décodés pour générer les signaux de commande. Ce qui vient d'être dit est une simplification. En vérité, certaines instructions ne sont pas encodées dans le microcode. Pour ces instructions, l'opération est récupérée directement dans l'opcode de l'instruction. C'est le cas des opérations d'addition, soustraction, les opérations logiques, les décalages et autres opérations gérées naturellement par l'ALU. Pour elles, l'opération à faire est extraite de l'opcode, pas du microcode. Pour ces opérations, le microcode encode l'opération à exécuter sur l'ALU par une micro-instruction généraliste nommée XI. La micro-opération XI indique qu'il faut activer le multiplexeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'unité de chargement et le program counter | prevText=L'unité de chargement et le program counter | next=L'implémentation matérielle des branchements | nextText=L'implémentation matérielle des branchements }} </noinclude> jl35jjta8gdt4vzkrddyvc3vanntkqv Fonctionnement d'un ordinateur/L'encodage des instructions 0 69029 773854 755771 2026-10-01T13:24:21Z Mewtow 31375 /* L'encodage des opérandes et des références */ 773854 wikitext text/x-wiki Pour rappel, les programmes informatiques sont placés en mémoire, au même titre que les données qu'ils manipulent. En clair, les instructions sont stockées dans la mémoire RAM/ROM sous la forme de suites de bits, tout comme les données. Il est impossible de faire la différence entre donnée et instruction, vu que rien ne ressemble plus à une suite de bits qu'une autre suite de bits. La seule différence est que les instructions sont chargées via le ''program counter'', alors que les données sont lues ou écrites par des instructions d'accès mémoire. En théorie, cette différence fait que le processeur ne peut pas prendre par erreur des instructions pour des données. ==La taille d'une instruction : taille fixe ou variable== Une instruction est codée sur plusieurs bits. Le nombre de bits utilisé pour coder une instruction est appelée la '''taille de l'instruction'''. Sur certains processeurs, la taille d'une instruction est fixe, c’est-à-dire qu'elle est la même pour toutes les instructions. Mais sur d'autres processeurs, les instructions n'ont pas toutes la même taille, ils gèrent des instructions de longueur variable. Un exemple de jeu d’instruction à longueur variable est le x86 des pc actuels, où une instruction peut faire entre 1 et 15 octets. L'encodage des instructions x86 est tellement compliqué qu'il prendrait à lui seul plusieurs chapitres ! Les instructions de longueur variable permettent d'économiser un peu de mémoire : avoir des instructions qui font entre 1 et 3 octets est plus avantageux que de tout mettre sur 3 octets. Mais en contrepartie le chargement de l'instruction suivante par le processeur est rendu plus compliqué. Le processeur doit en effet identifier la longueur de l'instruction courante pour savoir où est la suivante. À l'opposé, des instructions de taille fixe gâchent un peu de mémoire, mais permettent au processeur de calculer plus facilement l’adresse de l'instruction suivante et de la charger plus facilement. Il existe des processeurs qui sont un peu entre les deux, avec des instructions de taille fixe, mais qui ne font pas toutes la même taille. Un exemple est jeu d'instruction RISC-V, où il existe des instructions "normales" de 32 bits et des instructions "compressées" de 16 bits. Le processeur charge un mot de 32 bits, ce qui fait qu'il peut lire entre une et deux instructions à la fois. Au tout début de l'instruction, un bit est mis à 0 ou 1 selon que l'instruction soit longue ou courte. Le reste de l'instruction varie suivant sa longueur. ==L'encodage d'une instruction : généralités== Il est intéressant d'étudier comment les instructions sont encodées, à savoir comment sont organisés ses bits. En effet, une instruction n'est pas qu'une suite de bits sans signification. Elle est en fait découpée en '''champs''', à savoir des suites de bits qui ont une signification. ===L'opcode, aussi appelé "code opération"=== Toute instruction contient un champ opcode, qui indique quelle est l'instruction : est-ce une instruction d'addition, de soustraction, un branchement inconditionnel, un appel de fonction, une lecture en mémoire, etc. Le terme opcode est une abréviation pour "code opération". Typiquement, l'opcode est encodé sur un octet, voire moins. Plus l'opcode est encodé sur un grand nombre de bits, plus le processeur peut supporter un grand nombre d'instruction. Par exemple, un octet d'opcode permet d'encoder 256 instructions différentes, 5 bits d'opcode seulement 32 instructions, etc. Pour la même instruction, l'opcode peut être différent suivant le processeur, ce qui est source d'incompatibilités. Par exemple, les opcodes de l'instruction d'addition ne sont pas les mêmes sur les processeurs x86 (ceux de nos PC) et les anciens macintosh, ou encore les microcontrôleurs. Ce qui fait qu'un opcode de processeur x86 n'aura pas d'équivalent sur un autre processeur, ou correspondra à une instruction totalement différente. En théorie, l'opcode a une taille fixe, ce qui veut dire qu'il est toujours codé sur le même nombre de bits, pour toutes les instructions. Par exemple, sur les processeurs ARM7, l'opcode est encodé sur 8 bits. Il n'y a pas d'instruction qui a un opcode de 5 bits, une autre qui a un opcode de 8 bits, etc. Non, toutes les instructions ont un opcode de 8 bits. Mais il y a des jeux d'instruction qui font exception. Par exemple, sur les processeurs x86, l'opcode peut faire 1, 2 ou 3 octets. Le mécanisme pour gérer des opcodes de taille variable sera expliqué plus tard dans ce chapitre. Il arrive que de rares instructions ne soient composées d'un opcode, sans rien d'autre. Elles ont alors une représentation en binaire qui est unique. : Pour l'anecdote, certains processeurs n'utilisent qu'une seule et unique instruction, et qui peuvent se passer d'opcodes. Mais ce sont des curiosités, pas quelque chose d'utilisé en pratique. ===L'encodage des opérandes et des références=== En plus de l'opcode, une instruction doit préciser où sont ses opérandes. Par opérande, on veut parler des données manipulées par l'instruction. Pour cela, il y a deux cas possibles. * Le premier encode l'opérande dans l'instruction, avec l'adressage immédiat. L'opérande est alors une constante connue à la compilation, elle est intégrée dans l'instruction directement, après l'opcode. * Le second cas indique où se trouve l'opérande, en donnant une référence vers celle-ci. La référence précise dans quel registre ou à quelle adresse se trouve l'opérande. La référence a un champ dédié, rien que pour elle, à la suite de l'opcode. La référence est associée à un mode d'adressage qui précise comment l'interpréter : si c'est un numéro de registre, une adresse mémoire, un pointeur, de quoi calculer l'adresse, etc. Mais laissons de côté le mode d'adressage pour le moment et concentrons-nous sur l'encodage de l'opérande ou de sa référence. Les références n'ont pas toutes la même taille : une adresse utilise plus de bits qu'un numéro de registre, par exemple. En conséquence, une instruction dont les deux références sont des adresses mémoire prendra plus de place qu'un calcul entre deux registres. Les constantes immédiates ont souvent une taille proche de celle des adresses. En effet, les adresses mémoire sont un peu l'équivalent des constantes immédiates, mais pour les instructions d'accès mémoire. Un problème est que les instructions n'ont pas le même nombre d'opérandes. Les instructions arithmétiques et logiques sont souvent des '''instructions dyadiques''', à savoir qui prennent deux opérandes et fournissent un résultat. L'addition, la soustraction, les opérations bit à bit ET/OU/XOR, les décalages et rotations, sont dans ce cas. Il existe cependant quelques rares '''instructions monadiques''' qui n'ont qu'une seule opérande et fournissent un résultat, comme les instructions NOT, INC/DEC (incrémentation/décrémentation). Vu qu'elles n'ont pas le même nombre d'opérande, le nombre de bits utilisé pour encoder l'instruction ne sera pas le même. Ne parlons pas des opérations d'accès mémoire qui n'ont ni opérande ni résultat. Elles copient une donnée d'une source vers une destination. Par exemple, l'instruction MOV copie une donnée d'un registre source vers un registre destination, l'instruction LOAD charge une donnée depuis une adresse source vers un registre destination, etc. Et la source comme la destination doivent être encodés dans l'instruction, ce qui est équivalent à encoder deux opérandes. Les instructions de longueur variables permettent de résoudre ce problème de longueur. Une instruction de longueur variable prend autant de bits qu'elle a besoin pour encoder ses opérandes et son résultat, ou pour encoder sa source et de sa destination. Une instruction dyadique prendra donc plus de place qu'une instruction monadique. Mais les instructions de longueur fixe se débrouillent autrement. Elles se calent sur le pire des cas, l'instruction la plus longue, et doivent trouver un moyen de tout faire rentrer dans 16, 32 ou 64 bits. Généralement, elles réduisent les possibilités d'adressage, certaines opérandes sont encodées de manière implicite, on empêche certaines combinaisons d'opérandes, etc. Leur encodage est donc soit plus compliqué, soit plus restrictif. Une solution souvent utilisée réduit la taille des opérations dyadiques. Intuitivement, encoder une opération dyadique demande d'encoder trois références : une par opérande, plus une pour le résultat. Le résultat est ce qu'on appelle à tord l'encodage 3-opérandes, en confondant le résultat avec une opérande. Une autre dénomination tout aussi mauvaise est l'encodage encodage 3-adresses car une telle instruction encode 3 adresses, trois noms de registre, etc. Nous parlerons plutôt d''''encodage 3-références''', plus correct, qui indique bien que l’instruction encode trois références. Ce n'est pas parfait car l'une de ces opérandes peut très bien être une constante immédiate, mais passons. Mais encoder 2 opérandes + 1 résultat prend de la place. L'encodage 3-référence peut être modifié de manière à adresser le résultat de manière implicite. Pour économiser un peu de place, l'idée est d'adresser le résultat de manière implicite. Avec cet encodage, le résultat est enregistré dans le registre de la seconde opérande. Pour le dire autrement, l'opérande sera remplacé par le résultat de l'instruction, l'opérande est écrasé. Avec cet encodage, les instructions ne précisent que deux opérandes, pas le résultat, ce qui permet de gagner de la place. On parle alors d''''encodage 2-références''', d'encodage 2-adresses, ou encore d'encodage 2-opérandes. Un exemple d'utilisation est celui des processeurs RISC-V, qu'on a mentionné plus haut. Nous avions dit qu'ils géraient des instructions 32 bits, et des instructions compressées de 16 bits. Les instructions de 32 bits sont des instructions à trois adresses : elles indiquent deux opérandes et la destination du résultat. Les instructions de 16 bits n'ont que deux opérandes et encodent le résultat de manière implicite. Cela explique qu'elles soient plus courtes : deux opérandes prennent moins de place que trois. L'encodage 2-références peut aussi être utilisé avec l'adressage immédiat. Une instruction peut l'utiliser si un opérande est en adressage immédiat, les deux autres sont dans des registres. La raison est qu'une constante prend approximativement autant de bits qu'une adresse. Les deux sont souvent codées sur 16 bits, plus rarement 20/24 bits. Avec des instructions codées sur 32 bits, cela ne laisse qu'un ou deux octets pour coder l'opcode et les registres. L'encodage 2-opérande est alors une nécessité, économiser un numéro de registre est le seul moyen d'avoir assez de place si la constante est codée sur 20/24 bits. Les instructions monadiques utilisent pas défaut l'encodage 2-références, pour préciser l'opérande et le résultat. Mais elles peuvent aussi utiliser l'encodage une-référence, qui est aux instructions monadiques ce qu'est l'encodage 2-référence aux instructions dyadiques. L'idée est que le résultat écrase l'opérande, il la remplace. Le résultat est écrit à la même adresse que l'opérande, ou il est écrit dans le même registre. L'avantage est qu'on a besoin de ne préciser qu'une seule opérande, pas deux. L'instruction est donc plus courte. [[File:CODE2 formatos.jpg|centre|vignette|upright=2|Exemples d'encodage d'instruction sur une taille fixe. </br> 0 - Opération sans opérande. </br> 1 - Instruction mono-opérande en encodage 1-référence. </br> 2 - Instruction de branchement. </br> 3 - Instruction dyadique en encodage 2-opérande. </br> 4 - Instruction dyadique en encodage 3-opérandes.]] ===L'encodage du mode d'adressage=== Il existe deux méthodes principales pour préciser le mode d'adressage utilisé par l'instruction, avec un cas intermédiaire. Dans le premier cas, l'instruction ne gère qu'un mode d'adressage par opérande. Par exemple, prenons le cas d'une instruction COPY qui copie un registre dans un autre. sur les processeurs dit RISC (''Reduced Instruction Set Computer'') , les instructions arithmétiques ne peuvent manipuler que des registres. Un autre cas est celui de l'instruction MOV, qui peut copier un registre ou une constante dans un registre destination. La destination est forcément un registre, pas besoin d'encoder le mode d'adressage pour le registre destination. Par contre, la source doit être encodée explicitement. Dans des cas pareils, le mode d'adressage est déduit automatiquement via l'opcode : on parle de '''mode d'adressage implicite'''. Dans un cas intermédiaire, il se peut que plusieurs instructions existent pour faire la même chose, mais avec des modes d'adressages différents. Le mode d'adressage est alors encodé dans l'opcode, ou en est déduit. Il s'agit d'un cas particulier d'adressage implicite. Un exemple est celui des processeurs ARM, qui ont plusieurs instructions d'addition. Une qui ne manipule que des registres, et adresse trois registres, avec mode adressage implicite. Une autre additionne une constante immédiate avec un registre, ce qui fait que la constante immédiate et les deux registres source/destination sont aussi à mode d'adressage implicite. [[File:Mips32 addi.svg|centre|vignette|upright=2|Exemple d'une instruction avec mode d'adressage implicite, appartenant au jeu d'instruction MIPS.]] Dans le second cas, les instructions gèrent plusieurs modes d'adressage par opérande. Par exemple, une instruction d'addition peut additionner soit deux registres, soit un registre et une adresse, soit un registre et une constante. Dans un cas pareil, l'instruction doit préciser le mode d'adressage utilisé, au moyen de quelques bits intercalés entre l'opcode et les opérandes. On parle de '''mode d'adressage explicite'''. Sur certains processeurs, chaque instruction peut utiliser tous les modes d'adressage supportés par le processeur : on dit que le processeur est orthogonal. [[File:Machine instructions.svg|centre|vignette|upright=2|Exemple d'une instruction avec mode d'adressage explicite.]] Maintenant, tout cela ne vaut que pour encoder le mode d'adressage d'une donnée, pas plus. Mais une instruction gère plusieurs données : ses opérandes, son résultat. Pour les accès mémoire, c'est sa source et de destination. Il faut donc préciser plusieurs modes d'adressage : un par opérande, plus un pour le résultat. Les instructions manipulant plusieurs opérandes peuvent parfois utiliser un mode d'adressage différent pour chaque opérande. Par exemple, une addition manipule deux opérandes, ce qui demande d'utiliser un mode d'adressage par opérande (dans le pire des cas). Il faut donc préciser plusieurs modes d'adressage : un par opérande, plus un pour le résultat. Intuitivement, on se dit qu'il faut utiliser un encodage explicite pour chacune d'entre elles. Idem pour le résultat. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande/résultat est adressée explicitement |- ! Opcode ! Mode d'adressage opérande 1 | Référence vers l'opérande 1 ! Mode d'adressage opérande 2 | Référence vers l'opérande 2 ! Mode d'adressage résultat | Référence vers le résultat |} L'encodage du mode d'adressage prend quelques bits, entre 1 et 4, pas plus. Il est donc intéressant de regrouper les modes d'adressage dans un même octet, dans un même champ. Faire ainsi sépare bien les références/opérandes d'un côté, et les bits de contrôle de l'autre, qui encodent l'instruction proprement dite. Et cela se marie très bien avec les instructions de longueur variable. Elles gardent un cœur de taille fixe auquel on colle des opérandes/références de taille variable. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande/résultat est adressée explicitement |- ! Opcode ! Mode d'adressage opérande 1 ! Mode d'adressage opérande 2 ! Mode d'adressage résultat | Référence vers l'opérande 1 | Référence vers l'opérande 2 | Référence vers le résultat |} Avec l'encodage 2-référence, on n'a pas à encoder explicitement la référence du résultat, ni son mode d'adressage. Cela fait gagner un peu de place. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande est adressée explicitement, le résultat est adressé implicitement |- ! Opcode ! Mode d'adressage opérande 1 ! Mode d'adressage opérande 2/résultat | Référence vers l'opérande 1 | Référence vers l'opérande 2 / le résultat |} Il s'agit là d'un usage de l'encodage implicite du mode d'adressage. Mais il y en a d'autres, comme nous le verrons dans ce qui suit. ===L'encodage de la taille des opérandes=== Enfin, une instruction encode la taille des données qu'elle manipule. C'est surtout utile pour les instructions d'accès mémoire. Il est en théorie possible de se limiter à des instructions mémoire qui lisent/écrivent des données de la même taille que les registres entiers, souvent égal à la taille d'un mot mémoire. Mais dans les faits, seules les architectures anciennes à adressage par mot font ça. De nos jours, les architectures à adressage par byte permettent de préciser qu'on veut lire un entier de 8 bits, 16 bits, 32 bits, 64 bits. Pour cela, la solution la plus simple est d'encoder la taille des données dans l'instruction. On rajoute un champ qui indique quelle est la taille des données à lire/écrire. Le champ n'encode pas un nombre comme 8, 16, 32, 64. À la place, il contient deux bits qui sont à interpréter comme suit : * 00 pour des données de 8 bits ; * 01 pour des données de 16 bits ; * 10 pour des données de 32 bits ; * 11 pour des données de 64 bits. En général, ce champ n'existe que pour les instructions d'accès mémoire, qui sont les seules qui se préoccupent de la taille des opérandes. La taille des données est surtout pertinente quand on lit/écrit en mémoire RAM/ROM. Les opérations arithmétiques sont rarement concernées, car elles manipulent le plus souvent des registres, qui n'ont qu'une seule taille. Il y a des exemples où c'est le cas, comme avec le système d'''alias'' de registres des processeurs x86, mais c'est l'exception plus que la règle. Et de plus, ça ne sert que très rarement de limiter la taille des résultats, surtout qu'une simple opération de masquage peut toujours être employée pour éliminer les bits de poids fort. Quelques jeux d'instructions ajoutent ce système de taille des opérandes pour les opérations arithmétiques simples, comme les additions et soustractions. Il arrive même que certaines opérations gèrent la taille des opérandes, mais pas d'autres, sur certaines jeux d'instruction irréguliers. La raison est une question de compatibilité, par exemple quand un jeu d'instruction de 16 bits a été étendu au 32 bits. Les anciennes instructions gardent leur encodage et travaillent sur 16 bits, d'autres sont ajoutées pour gérer les opérandes 32 bits. Les processeurs x86 utilisent un système totalement différent, avec un pseudo-aliasing des registres, qu'on a vu dans le chapitre sur les registres. ==Les restrictions sur les modes d'adressage== Intuitivement, on se dit que toute opérande peut utiliser tous les modes d'adressage possibles, il n'y a pas de restrictions particulières. Cependant, ce n'est pas le cas du tout. Permettre à toutes les opérandes d'utiliser tous les modes d'adressage possibles pose quelques problèmes, qu'on ne peut pas résoudre facilement. ===La gestion du mode d'adressage immédiat=== Le premier problème est que de nombreuses combinaisons sont interdites, voire n'ont aucun sens. Par exemple, ça n'aurait aucun sens d'utiliser l'adressage immédiat pour le résultat d'une instruction. De même, ça n'aurait aucun intérêt d'utiliser le mode d'adressage immédiat pour les deux opérandes d'une opération dyadique. Concrètement, le mode d'adressage immédiat ne peut être utilisé que sur un opérande à la fois. De plus, l'adressage immédiat n'a de sens que pour les opérations dyadiques, mais n'a aucun sens avec les opérations monadiques. Une opération sur une constante donnera toujours le même résultat, autant éliminer l'instruction et précalculer ce résultat. En somme, seules les instructions dyadiques peuvent encoder une constante immédiate, pas les autres. Les instructions d'accès mémoire peuvent utiliser une variante de l'adressage immédiat : l'adressage absolu qui encode une adresse directement dans l'instruction. Mais l'encodage est assez similaire. ===Les restrictions de registres source/destination=== Les anciens processeurs avaient des restrictions quant à l'utilisation des registres. Par exemple, certaines instructions ne pouvaient utiliser que certains registres, pas les autres. D'autres avaient un registre de destination prévu à l'avance, d'autres ne pouvaient lire leurs opérandes que dans des registres précis, etc. Voyons cela avec quelques exemples. Les contraintes peuvent porter sur le registre pour le résultat. Un exemple est celui des anciennes architectures MIPS, qui limitaient le registre de résultat pour la multiplication. Les multiplications pouvaient lire leurs opérandes dans tous les registres, mais leur résultat allait dans un registre dédié, séparé des registres généraux, appelé HO/LO. Le registre était en réalité composé de deux registres : le registre LO pour les 32 bits de poids faible du résultat, le registre HO pour les 32 bits de poids fort. Les contraintes peuvent aussi porter sur les opérandes. Elles ne peuvent alors provenir que de certains registres, pas des autres. Et les restrictions peuvent porter sur les deux opérandes, ou alors uniquement sur la première opérande, ou que sur la seconde. Pour donner un exemple, prenons l'exemple des processeurs x86, avec les instructions de décalage. Elle prennent deux opérandes : l'opérande à décaler et le nombre de rangs par lequel il faut décaler. Le nombre de rangs est soit une constante immédiate, soit le registre CL. Les autres registres ne peuvent pas être utilisés pour préciser de combien il faut décaler. La même architecture avait des limitations similaires sur les très anciens processeurs x86, avant l'arrivée du CPU 386. Les opérations de multiplication et d'extension de signe devaient avoir leur première opérande dans le registre AX. De telles contraintes visent à réduire la taille des instructions. Pour reprendre le cas de l'instruction SHL, limiter le nombre de registre opérande à un seul fait qu'on peut l'adresser implicitement. ON n'a pas à encoder le numéro de registre, ce qui économise pas mal de bits. Cela permet à l'instruction de tenir sur un seul octet, deux si l'instruction utilise une constante immédiate. Un autre exemple de contrainte est celui de l'ancien processeur Data General Nova. Il disposait de 4 registres généraux (appelés improprement accumulateurs), mais seul deux d'entre eux servaient pour l'adressage mémoire. Précisément, le processeur gérait trois modes d'adressage, dont l'adressage absolu indicé. Pour rappel, celui-ci additionne une adresse fixe avec un indice variable. L'adresse est intégrée à l'instruction, alors que l'indice est dans un registre. Et sur le Data General Nova, l'indice ne pouvait être que dans les deux derniers registres, pas les deux premiers. ===Les restrictions sur les opérandes mémoire=== Une seconde restriction est liée à la source des opérandes. Un opérande peut provenir de deux sources : des registres, de la mémoire RAM, ou être fournie via adressage immédiat. Le résultat peut lui être enregistré en RAM ou dans les registres. Et cela met en avant un second problème : en faisant cela, une instruction peut effectuer plusieurs accès mémoire. Par exemple, il est possible de lire deux opérandes en mémoire RAM et d'enregistrer le résultat en mémoire RAM. Ou encore de lire deux opérandes en RAM et d'enregistrer le résultat dans les registres. Les processeurs gèrent ce problème de deux manières différentes. La première interdit de faire plusieurs accès mémoire par instruction, ce qui impose des limitations quant à l'utilisation des modes d'adressage. Typiquement, une instruction peut lire un opérande en RAM, mais pas plus. Le jeu d'instruction interdit certaines combinaisons de modes d'adressage avec deux opérandes. La seconde méthode autorise des instructions à faire plusieurs accès mémoire par instruction. Par exemple, une opération dyadique peut lire deux opérandes en RAM, copier une donnée d'une adresse mémoire à une autre, etc. Les modes d'adressages sont moins limités, des combinaisons deviennent possibles. Dans ce qui va suivre, nous allons séparer les instructions en plusieurs types : celles qui ne font pas d'accès mémoire, celles qui en font un, celles qui en font plusieurs. Nous parlerons d''''instruction simple-accès''' pour celles qui font au maximum un accès mémoire. Les instructions qui effectuent plusieurs accès mémoire seront appelées des '''instructions multi-accès'''. Et nous allons les voir séparément. ==L'encodage des instructions simple-accès== Pour commencer, nous allons voir l'encodage des instructions sur les processeurs qui ne gèrent pas plusieurs accès mémoire par instruction. Leur encodage est plus compact que pour les instructions multi-accès. ===Les instructions mémoire simple-accès et leur encodage=== Sur la plupart des processeurs, les instructions ne peuvent faire qu'un seul accès mémoire. Il est interdit de faire plusieurs accès mémoires par instruction. La première conséquence est que les seules instructions d'accès mémoire permises sont : * les instructions LOAD pour charger une donnée de la RAM dans un registre ; * l'instruction STORE pour copier une donnée d'un registre vers la RAM ; * l'instruction MOV en adressage inhérent (registre), qui copie un registre dans un autre ; * l'instruction MOV en adressage immédiat (constante) pour charger une constante immédiate dans un registre destination. D'autres instructions sont possibles, mais elles sont rarement implémentées. Par exemple, on peut citer l'instruction d'échange entre registres XCHG. Une autre possibilité est une variante de l'instruction STORE en mode d'adressage immédiat. Elle copie une constante dans une adresse mémoire. Elle n'est pas présente sur les architectures ARM, POWER PC ou SPARC. Mais laissons-les de côté. Pour rappel, les instructions d'accès mémoire copient une donnée d'une source vers une destination, les deux ayant leur propre mode d'adressage. Il n'est pas systématiquement encodé. Par exemple, l'instruction LOAD charge une donnée dans un registre destination, l'adressage de la destination est donc toujours l'adressage inhérent (nom de registre). Pas besoin d'encoder le mode d'adressage, juste la référence, le nom de registre. Idem pour les autres instructions. {|class="wikitable" |- ! colspan="7" | Instruction LOAD |- | Opcode | Adressage de la source * Adressage absolu (adresse) * Adressage indirect à registre * Adressage base + indice * etc. | Adresse ou référence de la source | | Nom de registre de destination |- ! colspan="7" | Instruction STORE |- | Opcode | | Nom de registre source | Adressage de la destination * Adressage absolu (adresse) * Adressage indirect à registre * Adressage base + indice * etc. | Adresse ou référence de destination |- ! colspan="7" | Instruction MOV |- | rowspan="2" | Opcode | rowspan="2" | * Adressage inhérent (registre) * Adressage immédiat (constante) | Nom de registre source | rowspan="2" | | rowspan="2" | Nom de registre destination |- | Constante immédiate |} Les processeurs x86, et sans doute quelques autres, fusionnent les instructions LOAD, STORE et MOV en une seule instruction appelée MOV. Elle est capable de faire une lecture, une écriture et une copie entre registres. Par contre, elle n'est pas capable de faire une copie d'une adresse mémoire vers une autre. Elles encodent une source et une destination, dont au moins l'une d'entre elle est un registre. Leur encodage place le registre juste après l'opcode, et la seconde opérande après. Un bit de l'instruction dit si la première opérande est la source ou la destination. {|class="wikitable" |+ Encodage d'une instruction MOV généraliste |- | rowspan="3" | Opcode | rowspan="3" | Bit qui indique l'ordre des champs | rowspan="3" | Nom de registre source/destination | rowspan="3" | * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source/destination |- | Adresse mémoire ou registres pour les pointeurs |- | Constante immédiate |} ===Les instructions dyadiques simple-accès et leur encodage=== Les instructions arithmétiques et logique sont elles aussi concernées par la contrainte d'un accès mémoire par instruction. Pour ces opérations, il y a deux possibilités : lire un opérande en RAM, enregistrer le résultat en RAM. Les deux options peuvent s'émuler avec deux instructions consécutives. La première option remplace une instruction LOAD suivie d'une instruction arithmétique/logique, la seconde remplace une instruction arithmétique/logique suivie d'une instruction STORE. Dans les deux cas, on économise un registre : le registre pour charger l'opérande dans le premier cas, le registre pour le résultat dans le second cas. Dans les faits, la seconde option n'est jamais appliquée, le résultat est systématiquement stocké dans un registre. En effet, il est fréquent qu'un opérande soit chargé depuis la mémoire, alors qu'il est rare qu'un résultat soit écrit en mémoire après avoir été calculé. Un résultat calculé a de fortes chances d'être réutilisé par la suite, donc de servir d'opérandes sous peu. Mieux vaut le laisser dans les registres, plutôt que de payer le prix d'un accès mémoire. Lire un opérande en mémoire RAM est par contre beaucoup plus fréquent et donc intéressant. [[File:Isregmem.png|vignette|Instruction dyadique de type ''load-op''.]] Dans ce qui suit, nous parlerons d''''instructions ''load-op''''' pour désigner de telles instructions. Elles lisent un opérande en RAM, mais l'autre opérande est lu depuis les registres et le résultat est enregistré dans les registres. Elles sont opposées aux '''instructions reg-reg''', ce qui est une abréviation pour registre-registre. Ces dernières lisent leurs deux opérandes dans les registres, idem pour l'écriture du résultat. Il faut aussi citer les '''instructions cst-reg''', où un opérande est fourni via adressage immédiat, l'autre opérande est dans les registres. Avec la contrainte d'un seul accès RAM par instruction, ce sont les seuls types d'instruction qui existent. {|class="wikitable" |+ Encodage d'une instruction dyadique reg-reg |- ! rowspan="2" | Opcode | rowspan="2" | * Adressage inhérent (registre) * Adressage immédiat (constante) | Nom de registre pour l'opérande 1 | rowspan="2" | Nom de registre pour l'opérande 2 | rowspan="2" | Nom de registre pour le résultat |- | Constante immédiate |} {|class="wikitable" |+ Encodage d'une instruction ''load-op'' |- ! rowspan="3" | Opcode | rowspan="3" | * Adressage inhérent (registre) * Adressage immédiat (constante) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source | rowspan="3" | | rowspan="3" | Nom de registre destination |- | Constante immédiate |- | Adresse mémoire ou registres pour les pointeurs |} Encoder une instruction ''load-op'' est compliqué. En soit, encoder l'opcode et les numéros de registre ne prend pas beaucoup de place, mais encoder une adresse complète prend beaucoup de bits. Une première solution utilise des instructions de taille variable : les instructions reg-reg sont plus courtes que les instructions ''load-op'', ces dernières prenant plus de place pour coder une adresse. Mais gérer des instructions de taille variable est complexe. Aussi, il existe une solution qui marche avec des instructions de taille fixe, à savoir si toutes les instructions sont codées sur le même nombre d'octets (souvent 2, 4 ou 8 octets selon le processeur) : utiliser l'encodage 2-opérandes. ==Les instructions multi-accès et leur encodage== Passons maintenant aux instructions multi-accès. Nous allons séparer les instructions d'accès mémoire d'un côté, et les instructions dyadiques de l'autre. ===Les instructions mémoire-mémoire=== Les instructions que nous allons voir dans ce qui suit sont des instructions d'accès mémoire, qui sont en plus multi-accès. Elles sont connues sous le nom d''''instruction mémoire-mémoire''', ce qui signifie qu'elle lisent une donnée en mémoire, pour l'enregistrer en mémoire. Les instructions mémoire-mémoire les plus communes sont les '''instructions de copie mémoire''', où une adresse est copiée dans une autre. Les copies en mémoire sont des opérations très fréquentes, il est très fréquent qu'un programme copie un bloc de mémoire dans un autre et beaucoup de programmeurs ont déjà été confronté à un tel cas. Aussi, les processeurs ajoutent des instructions multi-accès pour accélérer ces copies, ce qui fait un bon compromis entre performance et simplicité d'implémentation. Avec elles, la source et la destination sont systématiquement des adresses, mais, il faut préciser leur mode d'adressage : absolu, pointeur, autre. Pour préciser le mode d'adressage, il est possible de préciser un mode d’adressage différent pour la source et de la destination. Par exemple, on peut utiliser le mode d'adressage base + indice pour la source, l'adressage absolu pour la destination. {|class="wikitable" |+ Encodage d'une instruction de copie mémoire |- ! Opcode | Mode d'adressage de la source * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice * etc. | Mode d'adressage de la destination * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice * etc. | Référence(s) vers la source | Référence(s) vers la destination |} Une autre solution utilise le même mode d'adressage pour la source et la destination. On perd en flexibilité, mais ce n'est pas si grave. Les instructions de copie mémoire sont souvent utilisées pour copier des tableaux, qui sont souvent adressés avec l'adressage base + indice ou indirect à registre. Par contre, l'encodage de l'instruction est plus simple, on économise quelques bits. {|class="wikitable" |+ Encodage d'une instruction de copie mémoire |- ! Opcode | Mode d'adressage de la source et de la destination * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice | Référence(s) vers la source | Référence(s) vers la destination |} Les instructions multi-accès précédentes demandent d'encoder deux adresses dans l'instruction, ainsi que le mode d'adressage pour ces deux adresses. Les instructions sont donc assez longues et ne tiennent pas dans une instruction de taille fixe, des instructions de taille variables sont utilisées. Quelques processeurs fusionnent les instructions de copie mémoire avec les instructions LOAD, STORE et MOV. Elles ont alors une '''instruction mémoire généraliste''', souvent appelée MOV. Elle est capable de faire une lecture, une écriture, une copie entre registres et une copie d'une adresse mémoire vers une autre. Source comme destination doivent encoder le mode d'adressage adéquat, elles n'utilisent pas le même mode d'adressage. Typiquement, le premier opérande désigne la source et l'autre la destination. * Si les deux opérandes sont des registres, le premier registre est copié dans le second. * Si le premier opérande est un registre et l'autre une adresse, c'est une écriture. * Si le premier opérande est une adresse et l'autre un registre, c'est une lecture. * Si les deux opérandes sont des adresses, c'est une copie mémoire. {|class="wikitable" |+ Encodage d'une instruction mémoire généraliste |- ! rowspan="3" | Opcode | rowspan="3" | Mode d'adressage de la source * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) * Adressage immédiat (constante) | rowspan="3" | Mode d'adressage de la destination * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source | Nom de registre destination |- | Adresse mémoire ou registres pour les pointeurs | Adresse mémoire ou registres pour les pointeurs |- | Constante immédiate |} ===Les instructions multi-accès monadiques et dyadiques=== Il existe de rares instructions multi-accès monadiques. Un exemple est celui des instructions de décalage sur les CPU x86. L'opérande à décaler est lue soit depuis les registres, soit depuis la mémoire, elle est écrite au même endroit. Elle fait donc deux accès mémoire, si l'opérande est en RAM : un pour lire d'opérande, l'autre pour écrire le résultat. L'instruction fournit une seule adresse ou un seul numéro de registre, qui sert à la fois pour la source et pour la destination, pour l'opérande et le résultat. Il s'agit donc d'un encodage 1-référence, une sorte d'équivalent de l'encodage 2-référence pour les instructions monadiques. Les instructions monadiques de ce type, qui lisent une opérande en RAM et écrivent un résultat en RAM, au même endroit, porte un nom spécifique. Elles sont mine de rien assez courante. Tous les processeurs modernes en supportent quelques unes. Mais nous ne pouvons pas en parler à ce moment là du cours. La raison est que ces instructions sont toutes des instructions utilisées sur les processeurs multicœurs, dans des contextes très spécifiques, que nous n'avons pas encore abordé. Pour rentrer dans le détail, il s'agit d'une sous-classe de ces fameuses instructions atomiques appelées '''instructions ''read-modify-write'''''. Tout cela deviendra plus clair une fois qu'on en sera au chapitre sur les instructions atomiques. Les processeurs peuvent gérer quelques instructions multi-accès dyadiques. De rares instructions lisent deux opérandes en mémoire, mais le résultat est enregistré dans les registres. Par exemple, sur les processeurs x86, l'instruction de comparaison CMPS peut lire deux opérandes en mémoire RAM et les comparer. Le résultat de la comparaison est mémorisé dans le registre d'état, pas ailleurs, qui est adressé implicitement. L'instruction lit ses opérandes dans la mémoire, pas ailleurs. Enfin, il existe de rares processeurs où les opérandes peuvent être lus depuis les registres ou la RAM, idem pour l'écriture du résultat qui peut se faire dans les registres ou en RAM. De tels processeurs ont tous des instructions de taille variable. En effet, les instructions vont d'instructions qui encodent deux/trois registres, à des instructions encodant deux/trois adresses. Autant les premières sont très courtes, autant les autres sont très longues. La seule solution pratique est d'utiliser des instructions de taille variable. ==Le cas des processeurs x86== Après avoir vu la théorie, nous allons étudier l'encodage des instructions des CPU x86, présents dans nos PC. Ils ont un des encodage les plus complexe qui soit. Ils utilisent des instructions de longueur variable, qui font de 1 à 15 octets. Le jeu d'instruction contient aussi un grand nombre d'instructions, qui n'a eu de cesse d'augmenter avec le temps. Mais surtout, les instructions en question sont assez complexes. Il supporte des instructions cst-reg, reg-reg et ''load-op'', mais aussi quelques instructions multi-accès. Nous allons d'abord voir les instructions les plus complexes, histoire de voir un exemple concret d'instructions multi-accès, avant de voir comment sont encodées les instructions en général. ===L'encodage d'une instruction x86=== L'encodage d'une instruction x86 contient au minimum un opcode, qui peut-être complété avec des informations annexes. Premièrement, il peut être précédé d'un préfixe, qui complète l'opcode. Il modifie l'interprétation de l'opcode ou des octets qui suivent. Il existe un paquet de préfixes très différents dont nous ne pouvons pas encore parler ici. Il y a notamment un préfixe spécifique pour les instructions 64 bits. Suivant le préfixe, l'opcode peut passer de 1 à 3 octets. Il peut y avoir au maximum 4 préfixes, chacun codé sur un octet. Ils sont facultatifs. Deuxièmement, l'opcode peut être suivi par un octet qui précise le mode d'adressage. Il est facultatif car certaines instructions utilisent uniquement le mode d'adressage implicite. Il est appelé l'octet ModR/M. Troisièmement, l'octet ModR/M peut être suivi par trois informations, qui sont utilisées pour calculer l'adresse de l'opérande chargée depuis la mémoire. Elles regroupent un octet SIB pour l'adressage base + indice, un ''offset'', pour les adressages base + ''offset'' et/ou une constante immédiate. Les trois peuvent être combinées si le mode d'adressage des deux opérandes le permet, mais cela demande d'avoir une opérande immédiate couplée à une opérande en mode d'adressage base + indice + ''offset''. Ce n'est pas très courant. {|class="wikitable" |- ! Préfixes !! opcode !! Mod R/M !! SIB !! Displacement !! Immediate |- | Préfixes || opcode || Mode d'adressage || registres pour l'adressage base + indice || ''offset'' || Constante immédiate |- | facultatif || obligatoire || facultatif || facultatif || facultatif |- | 0 à 4 octets || 1, 2, 3 octets, souvent un seul || 1 octet || 1 octet || 1, 2 ou 4 octets || 1, 2, 4 octets en 32 bits, peut aller jusqu'à 8 en 64 bits. |} L'octet SIB inclu deux numéros de registres : un pour le registre de base, un autre pour le registre d'indice. Il inclu aussi la taille de l'opérande, qui est utilisée pour multiplier l'indice, avant de l'additionner au registre de base. {|class="wikitable" |+ Octet SIB |- ! Taille de l'opérande !! Base !! Indice |- | Entier || Numéro de registre || Numéro de registre |- | 2 bits || 3 bits || 3 bits |} L'octet Mod R/M est composé de trois champs, appelés MOD, REG et R/M. MOD est codé sur deux bits et précise le mode d'adressage : * 00 pour l'adressage indirect à registre ou l'adressage base + indice. * 01 pour dire qu'un ''offset'' de 1 octet est présent. * 10 pour dire qu'un ''offset'' de 4 octets est présent. * 11 si les deux opérandes sont dans un registre. Encoder des numéros de registres sur 3 bits limite le processeur à 8 registres. Les CPU x86 s'en sont contenté jusqu’à la génération 32 bits. Lors du passage au 64 bits, 8 autres registres ont été ajouté. Pour ajouter des registres, l'octet SIB se voit ajouter deux bits, un par numéro de registre, ce qui fait qu'il est alors codé sur 10 bits. Mais cela n'est le cas que si l'octet d'opcode est précédé par l'octet de préfixe adéquat, le préfixe REX, VEX ou XOP. Pour les opérations dyadiques, REG encode le numéro de registre de la première opérande, R/M encode le numéro de registre pour la seconde opérande. Pour les opérations à une seule opérande, le champ REG est ignoré et le numéro de registre est dans le champ R/M. En clair, l'octet Mod R/M a une interprétation qui dépend de l'opcode. De plus, la destination est encodée via encodage 2-opérande. La destination est soit la première opérande, soit la seconde. Un bit de l'opcode permet de choisir, ce qui donne un peu de flexibilité à l'encodage 2-opérande. {|class="wikitable" |+ Octet SIB |- ! Taille de l'opérande !! Base !! Indice |- | Entier || Numéro de registre || Numéro de registre |- | 2 bits || 3 bits || 3 bits |} ===Les ''string instructions''=== Les instructions dont nous allons parler sont connues sous le nom d'''instructions de chaines de caractère'', bien qu'elles travaillent en réalité plus sur des tableaux ou des zones de mémoire. Les opérations arithmétiques et logiques ne peuvent pas lire deux opérandes en mémoire, la possibilité est donc limitée à quelques instructions d'accès mémoire bien précises. Les instructions de chaine de caractère peuvent se classer en deux types : celles qui ne font qu'un seul accès mémoire, et celles qui en font deux. Les instructions de chaine de caractère multi-accès sont les suivantes : * L'instruction MOVS copie une adresse mémoire dans une autre, c'est la seule instruction de copie mémoire sur ces CPU. * L'instruction de comparaison CMPS peut lire deux opérandes en mémoire RAM et les comparer. Le résultat de la comparaison est mémorisé dans le registre d'état, pas ailleurs. Les autres instructions de chaine de caractère regroupent trois instructions : * LOD charge une donnée dans le registre EAX, l'adresse de cette donnée est dans le registre ESI. * STO copie une donnée en mémoire RAM, la donnée est dans le registre EAX, l'adresse mémoire est dans le registre ESI. * SCA charge une donnée et la compare avec le registre EAX, l'adresse de la donnée est dans le registre ESI. Pour les échanges entre deux adresses mémoire, les processeurs x86 fournissent une instruction dédiée, appelée MOVS. L'adresse de la source est dans le registre ESI, l'adresse de destination est dans le registre EDI. Vous remarquerez que les registres utilisés sont fixés à l'avance, ce qui fait qu'ils sont adressés implicitement. Les deux registres sont incrémentés/décrémentés à chaque exécution de l'instruction, afin de pointer vers le mot mémoire suivant. Fait intéressant, les instructions mémoires peuvent être répétées automatiquement plusieurs fois, en leur ajoutant un préfixe, un octet placé avant l'opcode. Le nombre de répétitions doit être stocké dans le registre ECX, qui est décrémenté à chaque exécution de l'instruction. Une fois que le registre ECX atteint 0, l'instruction n'est plus répétée et on passe à l'instruction suivante. En clair, l'instruction décrémente automatiquement ce registre à chaque exécution et s'arrête quand il atteint zéro. Suivant le préfixe, d'autres conditions peuvent être ajoutées. Elle peut être répétée plusieurs fois avec l'ajout du préfixe REP. L'instruction avec préfixe, REPMOVS, permet donc de copier un bloc de mémoire dans un autre, de copier N adresses consécutives ! Cela permet de parcourir la mémoire dans l'ordre montant (adresses croissantes) ou descendant (adresses décroissantes), suivant que les registres sont incrémentés ou décrémentés. La direction de parcours de la mémoire est spécifiée dans un bit incorporé dans le processeur, qui vaut 0 (décrémentation) ou 1 (incrémentation), et peut être altéré par des instructions dédiées, de manière à être mis à 1 ou 0. D'autres instructions d'accès mémoire peuvent être préfixées avec REP. Par exemple, l'instruction STOS, qui copie le contenu du registre EAX dans une adresse. On peut exécuter cette instruction une fois, ou la répéter plusieurs fois avec le préfixe REP. Là encore, on doit préciser l'adresse à laquelle écrire dans un registre spécifié, puis le nombre de répétitions dans le registre ECX. Là encore, le nombre de répétitions est décrémenté à chaque exécution, alors que l'adresse est incrémentée/décrémentée. L'instruction REPSTOS est très utile pour mettre à zéro une zone de mémoire, ou pour initialiser un tableau avec des données préétablies. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les modes d'adressage | prevText=Les modes d'adressage | next=Les jeux d'instructions | nextText=Les jeux d'instructions }} </noinclude> lr4jqp81ftcdi1m8dxuwre2ye37onij 773855 773854 2026-10-01T13:35:50Z Mewtow 31375 773855 wikitext text/x-wiki Pour rappel, les programmes informatiques sont placés en mémoire, au même titre que les données qu'ils manipulent. En clair, les instructions sont stockées dans la mémoire RAM/ROM sous la forme de suites de bits, tout comme les données. Il est impossible de faire la différence entre donnée et instruction, vu que rien ne ressemble plus à une suite de bits qu'une autre suite de bits. La seule différence est que les instructions sont chargées via le ''program counter'', alors que les données sont lues ou écrites par des instructions d'accès mémoire. En théorie, cette différence fait que le processeur ne peut pas prendre par erreur des instructions pour des données. ==La taille d'une instruction : taille fixe ou variable== Une instruction est codée sur plusieurs bits. Le nombre de bits utilisé pour coder une instruction est appelée la '''taille de l'instruction'''. Sur certains processeurs, la taille d'une instruction est fixe, c’est-à-dire qu'elle est la même pour toutes les instructions. Mais sur d'autres processeurs, les instructions n'ont pas toutes la même taille, ils gèrent des instructions de longueur variable. Un exemple de jeu d’instruction à longueur variable est le x86 des pc actuels, où une instruction peut faire entre 1 et 15 octets. L'encodage des instructions x86 est tellement compliqué qu'il prendrait à lui seul plusieurs chapitres ! Les instructions de longueur variable permettent d'économiser un peu de mémoire : avoir des instructions qui font entre 1 et 3 octets est plus avantageux que de tout mettre sur 3 octets. Mais en contrepartie le chargement de l'instruction suivante par le processeur est rendu plus compliqué. Le processeur doit en effet identifier la longueur de l'instruction courante pour savoir où est la suivante. À l'opposé, des instructions de taille fixe gâchent un peu de mémoire, mais permettent au processeur de calculer plus facilement l’adresse de l'instruction suivante et de la charger plus facilement. Il existe des processeurs qui sont un peu entre les deux, avec des instructions de taille fixe, mais qui ne font pas toutes la même taille. Un exemple est jeu d'instruction RISC-V, où il existe des instructions "normales" de 32 bits et des instructions "compressées" de 16 bits. Le processeur charge un mot de 32 bits, ce qui fait qu'il peut lire entre une et deux instructions à la fois. Au tout début de l'instruction, un bit est mis à 0 ou 1 selon que l'instruction soit longue ou courte. Le reste de l'instruction varie suivant sa longueur. ==L'encodage d'une instruction : généralités== Il est intéressant d'étudier comment les instructions sont encodées, à savoir comment sont organisés ses bits. En effet, une instruction n'est pas qu'une suite de bits sans signification. Elle est en fait découpée en '''champs''', à savoir des suites de bits qui ont une signification. ===L'opcode, aussi appelé "code opération"=== Toute instruction contient un champ opcode, qui indique quelle est l'instruction : est-ce une instruction d'addition, de soustraction, un branchement inconditionnel, un appel de fonction, une lecture en mémoire, etc. Le terme opcode est une abréviation pour "code opération". Typiquement, l'opcode est encodé sur un octet, voire moins. Plus l'opcode est encodé sur un grand nombre de bits, plus le processeur peut supporter un grand nombre d'instruction. Par exemple, un octet d'opcode permet d'encoder 256 instructions différentes, 5 bits d'opcode seulement 32 instructions, etc. Pour la même instruction, l'opcode peut être différent suivant le processeur, ce qui est source d'incompatibilités. Par exemple, les opcodes de l'instruction d'addition ne sont pas les mêmes sur les processeurs x86 (ceux de nos PC) et les anciens macintosh, ou encore les microcontrôleurs. Ce qui fait qu'un opcode de processeur x86 n'aura pas d'équivalent sur un autre processeur, ou correspondra à une instruction totalement différente. En théorie, l'opcode a une taille fixe, ce qui veut dire qu'il est toujours codé sur le même nombre de bits, pour toutes les instructions. Par exemple, sur les processeurs ARM7, l'opcode est encodé sur 8 bits. Il n'y a pas d'instruction qui a un opcode de 5 bits, une autre qui a un opcode de 8 bits, etc. Non, toutes les instructions ont un opcode de 8 bits. Mais il y a des jeux d'instruction qui font exception. Par exemple, sur les processeurs x86, l'opcode peut faire 1, 2 ou 3 octets. Le mécanisme pour gérer des opcodes de taille variable sera expliqué plus tard dans ce chapitre. Il arrive que de rares instructions ne soient composées d'un opcode, sans rien d'autre. Elles ont alors une représentation en binaire qui est unique. : Pour l'anecdote, certains processeurs n'utilisent qu'une seule et unique instruction, et qui peuvent se passer d'opcodes. Mais ce sont des curiosités, pas quelque chose d'utilisé en pratique. ===L'encodage des opérandes et des références=== En plus de l'opcode, une instruction doit préciser où sont ses opérandes. Par opérande, on veut parler des données manipulées par l'instruction. Pour cela, il y a deux cas possibles. * Le premier encode l'opérande dans l'instruction, avec l'adressage immédiat. L'opérande est alors une constante connue à la compilation, elle est intégrée dans l'instruction directement, après l'opcode. * Le second cas indique où se trouve l'opérande, en donnant une référence vers celle-ci. La référence précise dans quel registre ou à quelle adresse se trouve l'opérande. La référence a un champ dédié, rien que pour elle, à la suite de l'opcode. La référence est associée à un mode d'adressage qui précise comment l'interpréter : si c'est un numéro de registre, une adresse mémoire, un pointeur, de quoi calculer l'adresse, etc. Mais laissons de côté le mode d'adressage pour le moment et concentrons-nous sur l'encodage de l'opérande ou de sa référence. Les références n'ont pas toutes la même taille : une adresse utilise plus de bits qu'un numéro de registre, par exemple. En conséquence, une instruction dont les deux références sont des adresses mémoire prendra plus de place qu'un calcul entre deux registres. Les constantes immédiates ont souvent une taille proche de celle des adresses. En effet, les adresses mémoire sont un peu l'équivalent des constantes immédiates, mais pour les instructions d'accès mémoire. Un problème est que les instructions n'ont pas le même nombre d'opérandes. Les instructions arithmétiques et logiques sont souvent des '''instructions dyadiques''', à savoir qui prennent deux opérandes et fournissent un résultat. L'addition, la soustraction, les opérations bit à bit ET/OU/XOR, les décalages et rotations, sont dans ce cas. Il existe cependant quelques rares '''instructions monadiques''' qui n'ont qu'une seule opérande et fournissent un résultat, comme les instructions NOT, INC/DEC (incrémentation/décrémentation). Vu qu'elles n'ont pas le même nombre d'opérande, le nombre de bits utilisé pour encoder l'instruction ne sera pas le même. Ne parlons pas des opérations d'accès mémoire qui n'ont ni opérande ni résultat. Elles copient une donnée d'une source vers une destination. Par exemple, l'instruction MOV copie une donnée d'un registre source vers un registre destination, l'instruction LOAD charge une donnée depuis une adresse source vers un registre destination, etc. Et la source comme la destination doivent être encodés dans l'instruction, ce qui est équivalent à encoder deux opérandes. Les instructions de longueur variables permettent de résoudre ce problème de longueur. Une instruction de longueur variable prend autant de bits qu'elle a besoin pour encoder ses opérandes et son résultat, ou pour encoder sa source et de sa destination. Une instruction dyadique prendra donc plus de place qu'une instruction monadique. Mais les instructions de longueur fixe se débrouillent autrement. Elles se calent sur le pire des cas, l'instruction la plus longue, et doivent trouver un moyen de tout faire rentrer dans 16, 32 ou 64 bits. Généralement, elles réduisent les possibilités d'adressage, certaines opérandes sont encodées de manière implicite, on empêche certaines combinaisons d'opérandes, etc. Leur encodage est donc soit plus compliqué, soit plus restrictif. Une solution souvent utilisée réduit la taille des opérations dyadiques. Intuitivement, encoder une opération dyadique demande d'encoder trois références : une par opérande, plus une pour le résultat. Le résultat est ce qu'on appelle à tord l'encodage 3-opérandes, en confondant le résultat avec une opérande. Une autre dénomination tout aussi mauvaise est l'encodage encodage 3-adresses car une telle instruction encode 3 adresses, trois noms de registre, etc. Nous parlerons plutôt d''''encodage 3-références''', plus correct, qui indique bien que l’instruction encode trois références. Ce n'est pas parfait car l'une de ces opérandes peut très bien être une constante immédiate, mais passons. Mais encoder 2 opérandes + 1 résultat prend de la place. L'encodage 3-référence peut être modifié de manière à adresser le résultat de manière implicite. Pour économiser un peu de place, l'idée est d'adresser le résultat de manière implicite. Avec cet encodage, le résultat est enregistré dans le registre de la seconde opérande. Pour le dire autrement, l'opérande sera remplacé par le résultat de l'instruction, l'opérande est écrasé. Avec cet encodage, les instructions ne précisent que deux opérandes, pas le résultat, ce qui permet de gagner de la place. On parle alors d''''encodage 2-références''', d'encodage 2-adresses, ou encore d'encodage 2-opérandes. Un exemple d'utilisation est celui des processeurs RISC-V, qu'on a mentionné plus haut. Nous avions dit qu'ils géraient des instructions 32 bits, et des instructions compressées de 16 bits. Les instructions de 32 bits sont des instructions à trois adresses : elles indiquent deux opérandes et la destination du résultat. Les instructions de 16 bits n'ont que deux opérandes et encodent le résultat de manière implicite. Cela explique qu'elles soient plus courtes : deux opérandes prennent moins de place que trois. L'encodage 2-références peut aussi être utilisé avec l'adressage immédiat. Une instruction peut l'utiliser si un opérande est en adressage immédiat, les deux autres sont dans des registres. La raison est qu'une constante prend approximativement autant de bits qu'une adresse. Les deux sont souvent codées sur 16 bits, plus rarement 20/24 bits. Avec des instructions codées sur 32 bits, cela ne laisse qu'un ou deux octets pour coder l'opcode et les registres. L'encodage 2-opérande est alors une nécessité, économiser un numéro de registre est le seul moyen d'avoir assez de place si la constante est codée sur 20/24 bits. Les instructions monadiques utilisent pas défaut l'encodage 2-références, pour préciser l'opérande et le résultat. Mais elles peuvent aussi utiliser l'encodage une-référence, qui est aux instructions monadiques ce qu'est l'encodage 2-référence aux instructions dyadiques. L'idée est que le résultat écrase l'opérande, il la remplace. Le résultat est écrit à la même adresse que l'opérande, ou il est écrit dans le même registre. L'avantage est qu'on a besoin de ne préciser qu'une seule opérande, pas deux. L'instruction est donc plus courte. [[File:CODE2 formatos.jpg|centre|vignette|upright=2|Exemples d'encodage d'instruction sur une taille fixe. </br> 0 - Opération sans opérande. </br> 1 - Instruction mono-opérande en encodage 1-référence. </br> 2 - Instruction de branchement. </br> 3 - Instruction dyadique en encodage 2-opérande. </br> 4 - Instruction dyadique en encodage 3-opérandes.]] ===L'encodage du mode d'adressage=== Il existe deux méthodes principales pour préciser le mode d'adressage utilisé par l'instruction, avec un cas intermédiaire. Dans le premier cas, l'instruction ne gère qu'un mode d'adressage par opérande. Par exemple, prenons le cas d'une instruction COPY qui copie un registre dans un autre. sur les processeurs dit RISC (''Reduced Instruction Set Computer'') , les instructions arithmétiques ne peuvent manipuler que des registres. Un autre cas est celui de l'instruction MOV, qui peut copier un registre ou une constante dans un registre destination. La destination est forcément un registre, pas besoin d'encoder le mode d'adressage pour le registre destination. Par contre, la source doit être encodée explicitement. Dans des cas pareils, le mode d'adressage est déduit automatiquement via l'opcode : on parle de '''mode d'adressage implicite'''. Dans un cas intermédiaire, il se peut que plusieurs instructions existent pour faire la même chose, mais avec des modes d'adressages différents. Le mode d'adressage est alors encodé dans l'opcode, ou en est déduit. Il s'agit d'un cas particulier d'adressage implicite. Un exemple est celui des processeurs ARM, qui ont plusieurs instructions d'addition. Une qui ne manipule que des registres, et adresse trois registres, avec mode adressage implicite. Une autre additionne une constante immédiate avec un registre, ce qui fait que la constante immédiate et les deux registres source/destination sont aussi à mode d'adressage implicite. [[File:Mips32 addi.svg|centre|vignette|upright=2|Exemple d'une instruction avec mode d'adressage implicite, appartenant au jeu d'instruction MIPS.]] Dans le second cas, les instructions gèrent plusieurs modes d'adressage par opérande. Par exemple, une instruction d'addition peut additionner soit deux registres, soit un registre et une adresse, soit un registre et une constante. Dans un cas pareil, l'instruction doit préciser le mode d'adressage utilisé, au moyen de quelques bits intercalés entre l'opcode et les opérandes. On parle de '''mode d'adressage explicite'''. Sur certains processeurs, chaque instruction peut utiliser tous les modes d'adressage supportés par le processeur : on dit que le processeur est orthogonal. [[File:Machine instructions.svg|centre|vignette|upright=2|Exemple d'une instruction avec mode d'adressage explicite.]] Maintenant, tout cela ne vaut que pour encoder le mode d'adressage d'une donnée, pas plus. Mais une instruction gère plusieurs données : ses opérandes, son résultat. Pour les accès mémoire, c'est sa source et de destination. Il faut donc préciser plusieurs modes d'adressage : un par opérande, plus un pour le résultat. Les instructions manipulant plusieurs opérandes peuvent parfois utiliser un mode d'adressage différent pour chaque opérande. Par exemple, une addition manipule deux opérandes, ce qui demande d'utiliser un mode d'adressage par opérande (dans le pire des cas). Il faut donc préciser plusieurs modes d'adressage : un par opérande, plus un pour le résultat. Intuitivement, on se dit qu'il faut utiliser un encodage explicite pour chacune d'entre elles. Idem pour le résultat. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande/résultat est adressée explicitement |- ! Opcode ! Mode d'adressage opérande 1 | Référence vers l'opérande 1 ! Mode d'adressage opérande 2 | Référence vers l'opérande 2 ! Mode d'adressage résultat | Référence vers le résultat |} L'encodage du mode d'adressage prend quelques bits, entre 1 et 4, pas plus. Il est donc intéressant de regrouper les modes d'adressage dans un même octet, dans un même champ. Faire ainsi sépare bien les références/opérandes d'un côté, et les bits de contrôle de l'autre, qui encodent l'instruction proprement dite. Et cela se marie très bien avec les instructions de longueur variable. Elles gardent un cœur de taille fixe auquel on colle des opérandes/références de taille variable. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande/résultat est adressée explicitement |- ! Opcode ! Mode d'adressage opérande 1 ! Mode d'adressage opérande 2 ! Mode d'adressage résultat | Référence vers l'opérande 1 | Référence vers l'opérande 2 | Référence vers le résultat |} Avec l'encodage 2-référence, on n'a pas à encoder explicitement la référence du résultat, ni son mode d'adressage. Cela fait gagner un peu de place. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande est adressée explicitement, le résultat est adressé implicitement |- ! Opcode ! Mode d'adressage opérande 1 ! Mode d'adressage opérande 2/résultat | Référence vers l'opérande 1 | Référence vers l'opérande 2 / le résultat |} Il s'agit là d'un usage de l'encodage implicite du mode d'adressage. Mais il y en a d'autres, comme nous le verrons dans ce qui suit. ===L'encodage de la taille des opérandes=== Enfin, une instruction encode la taille des données qu'elle manipule. C'est surtout utile pour les instructions d'accès mémoire. Il est en théorie possible de se limiter à des instructions mémoire qui lisent/écrivent des données de la même taille que les registres entiers, souvent égal à la taille d'un mot mémoire. Mais dans les faits, seules les architectures anciennes à adressage par mot font ça. De nos jours, les architectures à adressage par byte permettent de préciser qu'on veut lire un entier de 8 bits, 16 bits, 32 bits, 64 bits. Pour cela, la solution la plus simple est d'encoder la taille des données dans l'instruction. On rajoute un champ qui indique quelle est la taille des données à lire/écrire. Le champ n'encode pas un nombre comme 8, 16, 32, 64. À la place, il contient deux bits qui sont à interpréter comme suit : * 00 pour des données de 8 bits ; * 01 pour des données de 16 bits ; * 10 pour des données de 32 bits ; * 11 pour des données de 64 bits. En général, ce champ n'existe que pour les instructions d'accès mémoire, qui sont les seules qui se préoccupent de la taille des opérandes. La taille des données est surtout pertinente quand on lit/écrit en mémoire RAM/ROM. Les opérations arithmétiques sont rarement concernées, car elles manipulent le plus souvent des registres, qui n'ont qu'une seule taille. Il y a des exemples où c'est le cas, comme avec le système d'''alias'' de registres des processeurs x86, mais c'est l'exception plus que la règle. Et de plus, ça ne sert que très rarement de limiter la taille des résultats, surtout qu'une simple opération de masquage peut toujours être employée pour éliminer les bits de poids fort. Quelques jeux d'instructions ajoutent ce système de taille des opérandes pour les opérations arithmétiques simples, comme les additions et soustractions. Il arrive même que certaines opérations gèrent la taille des opérandes, mais pas d'autres, sur certaines jeux d'instruction irréguliers. La raison est une question de compatibilité, par exemple quand un jeu d'instruction de 16 bits a été étendu au 32 bits. Les anciennes instructions gardent leur encodage et travaillent sur 16 bits, d'autres sont ajoutées pour gérer les opérandes 32 bits. Les processeurs x86 utilisent un système totalement différent, avec un pseudo-aliasing des registres, qu'on a vu dans le chapitre sur les registres. ===L'encodage des instructions est avant tout un compromis=== Encoder des instructions est quelque chose d'assez complexe, qui demande de faire un compromis entre des contraintes opposées, contradictoires. Pour savoir ce que doit faire un encodage idéal, faisons une petite liste. Idéalement, un encodage doit utiliser des instructions de taille fixe, afin de simplifier le chargement/décodage des instructions. Avec des instructions de taille variable, le processeur a plus de mal à déterminer où se trouve la prochaine instruction. Il doit déterminer la longueur de l'instruction pour savoir où se trouve la suivante. Alors qu'avec des instructions de taille fixe, le calcul est nettement plus simple, la longueur de l'instruction est connue à l'avance. Les instructions doivent aussi avoir une taille qui est une puissance de deux, afin de simplifier l'alignement des instructions en mémoire. Les instructions peuvent donc faire 8, 16, 32, 64 bits. Et là, voyons chaque taille au cas par cas. * Encoder des instructions sur 8 bits est juste impossible, il n'y a pas assez de bits. Sauf sur des jeux d'instructions spécialisés appelés des machines à pile, qui auront droit à un chapitre dédié. * Encoder des instructions sur 16 bits, c'est possible, mais il faut oublier l'usage de constantes immédiates ou l'adressage absolu, les constantes et adresses ne rentrant pas dans une instruction aussi petite. Concrètement, il faut utiliser des jeux d'instructions spéciaux, comme des architectures à accumulateur, qu'on verra dans quelques chapitres. * Encoder des instructions sur 32 bits est la règle, tous les processeurs font cela de nos jours. Il y a assez de place pour encoder des constantes immédiates et des adresses assez larges, disons 16 ou 24 bits. De même, encoder des instructions trois-opérandes utilisant des registres n'est pas un problème : 32 bits est suffisant pour encoder 4 registres et un opcode. * Encoder des instructions sur 64 bits serait complétement excessif. On aurait assez de place pour mettre plein d'instructions, pleins de registres, des constantes et adresses très larges. Par contre, doubler la taille des instructions signifie doubler la taille du code, les programmes prennent deux fois plus de place qu'avec des instructions 32 bits. Et il faut aussi deux fois plus de débit binaire pour charger le même nombre d'instructions, ce qui implique de doubler la taille du bus qui relie le processeur à la mémoire des instructions. Pour résumer, des instructions de 32 bits sont l'idéal : assez longues pour qu'on puisse encoder des instructions utiles, pas assez longue pour pose problème niveau matériel/mémoire. ==Les restrictions sur les modes d'adressage== Intuitivement, on se dit que toute opérande peut utiliser tous les modes d'adressage possibles, il n'y a pas de restrictions particulières. Cependant, ce n'est pas le cas du tout. Permettre à toutes les opérandes d'utiliser tous les modes d'adressage possibles pose quelques problèmes, qu'on ne peut pas résoudre facilement. ===La gestion du mode d'adressage immédiat=== Le premier problème est que de nombreuses combinaisons sont interdites, voire n'ont aucun sens. Par exemple, ça n'aurait aucun sens d'utiliser l'adressage immédiat pour le résultat d'une instruction. De même, ça n'aurait aucun intérêt d'utiliser le mode d'adressage immédiat pour les deux opérandes d'une opération dyadique. Concrètement, le mode d'adressage immédiat ne peut être utilisé que sur un opérande à la fois. De plus, l'adressage immédiat n'a de sens que pour les opérations dyadiques, mais n'a aucun sens avec les opérations monadiques. Une opération sur une constante donnera toujours le même résultat, autant éliminer l'instruction et précalculer ce résultat. En somme, seules les instructions dyadiques peuvent encoder une constante immédiate, pas les autres. Les instructions d'accès mémoire peuvent utiliser une variante de l'adressage immédiat : l'adressage absolu qui encode une adresse directement dans l'instruction. Mais l'encodage est assez similaire. ===Les restrictions de registres source/destination=== Les anciens processeurs avaient des restrictions quant à l'utilisation des registres. Par exemple, certaines instructions ne pouvaient utiliser que certains registres, pas les autres. D'autres avaient un registre de destination prévu à l'avance, d'autres ne pouvaient lire leurs opérandes que dans des registres précis, etc. Voyons cela avec quelques exemples. Les contraintes peuvent porter sur le registre pour le résultat. Un exemple est celui des anciennes architectures MIPS, qui limitaient le registre de résultat pour la multiplication. Les multiplications pouvaient lire leurs opérandes dans tous les registres, mais leur résultat allait dans un registre dédié, séparé des registres généraux, appelé HO/LO. Le registre était en réalité composé de deux registres : le registre LO pour les 32 bits de poids faible du résultat, le registre HO pour les 32 bits de poids fort. Les contraintes peuvent aussi porter sur les opérandes. Elles ne peuvent alors provenir que de certains registres, pas des autres. Et les restrictions peuvent porter sur les deux opérandes, ou alors uniquement sur la première opérande, ou que sur la seconde. Pour donner un exemple, prenons l'exemple des processeurs x86, avec les instructions de décalage. Elle prennent deux opérandes : l'opérande à décaler et le nombre de rangs par lequel il faut décaler. Le nombre de rangs est soit une constante immédiate, soit le registre CL. Les autres registres ne peuvent pas être utilisés pour préciser de combien il faut décaler. La même architecture avait des limitations similaires sur les très anciens processeurs x86, avant l'arrivée du CPU 386. Les opérations de multiplication et d'extension de signe devaient avoir leur première opérande dans le registre AX. De telles contraintes visent à réduire la taille des instructions. Pour reprendre le cas de l'instruction SHL, limiter le nombre de registre opérande à un seul fait qu'on peut l'adresser implicitement. ON n'a pas à encoder le numéro de registre, ce qui économise pas mal de bits. Cela permet à l'instruction de tenir sur un seul octet, deux si l'instruction utilise une constante immédiate. Un autre exemple de contrainte est celui de l'ancien processeur Data General Nova. Il disposait de 4 registres généraux (appelés improprement accumulateurs), mais seul deux d'entre eux servaient pour l'adressage mémoire. Précisément, le processeur gérait trois modes d'adressage, dont l'adressage absolu indicé. Pour rappel, celui-ci additionne une adresse fixe avec un indice variable. L'adresse est intégrée à l'instruction, alors que l'indice est dans un registre. Et sur le Data General Nova, l'indice ne pouvait être que dans les deux derniers registres, pas les deux premiers. ===Les restrictions sur les opérandes mémoire=== Une seconde restriction est liée à la source des opérandes. Un opérande peut provenir de deux sources : des registres, de la mémoire RAM, ou être fournie via adressage immédiat. Le résultat peut lui être enregistré en RAM ou dans les registres. Et cela met en avant un second problème : en faisant cela, une instruction peut effectuer plusieurs accès mémoire. Par exemple, il est possible de lire deux opérandes en mémoire RAM et d'enregistrer le résultat en mémoire RAM. Ou encore de lire deux opérandes en RAM et d'enregistrer le résultat dans les registres. Les processeurs gèrent ce problème de deux manières différentes. La première interdit de faire plusieurs accès mémoire par instruction, ce qui impose des limitations quant à l'utilisation des modes d'adressage. Typiquement, une instruction peut lire un opérande en RAM, mais pas plus. Le jeu d'instruction interdit certaines combinaisons de modes d'adressage avec deux opérandes. La seconde méthode autorise des instructions à faire plusieurs accès mémoire par instruction. Par exemple, une opération dyadique peut lire deux opérandes en RAM, copier une donnée d'une adresse mémoire à une autre, etc. Les modes d'adressages sont moins limités, des combinaisons deviennent possibles. Dans ce qui va suivre, nous allons séparer les instructions en plusieurs types : celles qui ne font pas d'accès mémoire, celles qui en font un, celles qui en font plusieurs. Nous parlerons d''''instruction simple-accès''' pour celles qui font au maximum un accès mémoire. Les instructions qui effectuent plusieurs accès mémoire seront appelées des '''instructions multi-accès'''. Et nous allons les voir séparément. ==L'encodage des instructions simple-accès== Pour commencer, nous allons voir l'encodage des instructions sur les processeurs qui ne gèrent pas plusieurs accès mémoire par instruction. Leur encodage est plus compact que pour les instructions multi-accès. ===Les instructions mémoire simple-accès et leur encodage=== Sur la plupart des processeurs, les instructions ne peuvent faire qu'un seul accès mémoire. Il est interdit de faire plusieurs accès mémoires par instruction. La première conséquence est que les seules instructions d'accès mémoire permises sont : * les instructions LOAD pour charger une donnée de la RAM dans un registre ; * l'instruction STORE pour copier une donnée d'un registre vers la RAM ; * l'instruction MOV en adressage inhérent (registre), qui copie un registre dans un autre ; * l'instruction MOV en adressage immédiat (constante) pour charger une constante immédiate dans un registre destination. D'autres instructions sont possibles, mais elles sont rarement implémentées. Par exemple, on peut citer l'instruction d'échange entre registres XCHG. Une autre possibilité est une variante de l'instruction STORE en mode d'adressage immédiat. Elle copie une constante dans une adresse mémoire. Elle n'est pas présente sur les architectures ARM, POWER PC ou SPARC. Mais laissons-les de côté. Pour rappel, les instructions d'accès mémoire copient une donnée d'une source vers une destination, les deux ayant leur propre mode d'adressage. Il n'est pas systématiquement encodé. Par exemple, l'instruction LOAD charge une donnée dans un registre destination, l'adressage de la destination est donc toujours l'adressage inhérent (nom de registre). Pas besoin d'encoder le mode d'adressage, juste la référence, le nom de registre. Idem pour les autres instructions. {|class="wikitable" |- ! colspan="7" | Instruction LOAD |- | Opcode | Adressage de la source * Adressage absolu (adresse) * Adressage indirect à registre * Adressage base + indice * etc. | Adresse ou référence de la source | | Nom de registre de destination |- ! colspan="7" | Instruction STORE |- | Opcode | | Nom de registre source | Adressage de la destination * Adressage absolu (adresse) * Adressage indirect à registre * Adressage base + indice * etc. | Adresse ou référence de destination |- ! colspan="7" | Instruction MOV |- | rowspan="2" | Opcode | rowspan="2" | * Adressage inhérent (registre) * Adressage immédiat (constante) | Nom de registre source | rowspan="2" | | rowspan="2" | Nom de registre destination |- | Constante immédiate |} Les processeurs x86, et sans doute quelques autres, fusionnent les instructions LOAD, STORE et MOV en une seule instruction appelée MOV. Elle est capable de faire une lecture, une écriture et une copie entre registres. Par contre, elle n'est pas capable de faire une copie d'une adresse mémoire vers une autre. Elles encodent une source et une destination, dont au moins l'une d'entre elle est un registre. Leur encodage place le registre juste après l'opcode, et la seconde opérande après. Un bit de l'instruction dit si la première opérande est la source ou la destination. {|class="wikitable" |+ Encodage d'une instruction MOV généraliste |- | rowspan="3" | Opcode | rowspan="3" | Bit qui indique l'ordre des champs | rowspan="3" | Nom de registre source/destination | rowspan="3" | * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source/destination |- | Adresse mémoire ou registres pour les pointeurs |- | Constante immédiate |} ===Les instructions dyadiques simple-accès et leur encodage=== Les instructions arithmétiques et logique sont elles aussi concernées par la contrainte d'un accès mémoire par instruction. Pour ces opérations, il y a deux possibilités : lire un opérande en RAM, enregistrer le résultat en RAM. Les deux options peuvent s'émuler avec deux instructions consécutives. La première option remplace une instruction LOAD suivie d'une instruction arithmétique/logique, la seconde remplace une instruction arithmétique/logique suivie d'une instruction STORE. Dans les deux cas, on économise un registre : le registre pour charger l'opérande dans le premier cas, le registre pour le résultat dans le second cas. Dans les faits, la seconde option n'est jamais appliquée, le résultat est systématiquement stocké dans un registre. En effet, il est fréquent qu'un opérande soit chargé depuis la mémoire, alors qu'il est rare qu'un résultat soit écrit en mémoire après avoir été calculé. Un résultat calculé a de fortes chances d'être réutilisé par la suite, donc de servir d'opérandes sous peu. Mieux vaut le laisser dans les registres, plutôt que de payer le prix d'un accès mémoire. Lire un opérande en mémoire RAM est par contre beaucoup plus fréquent et donc intéressant. [[File:Isregmem.png|vignette|Instruction dyadique de type ''load-op''.]] Dans ce qui suit, nous parlerons d''''instructions ''load-op''''' pour désigner de telles instructions. Elles lisent un opérande en RAM, mais l'autre opérande est lu depuis les registres et le résultat est enregistré dans les registres. Elles sont opposées aux '''instructions reg-reg''', ce qui est une abréviation pour registre-registre. Ces dernières lisent leurs deux opérandes dans les registres, idem pour l'écriture du résultat. Il faut aussi citer les '''instructions cst-reg''', où un opérande est fourni via adressage immédiat, l'autre opérande est dans les registres. Avec la contrainte d'un seul accès RAM par instruction, ce sont les seuls types d'instruction qui existent. {|class="wikitable" |+ Encodage d'une instruction dyadique reg-reg |- ! rowspan="2" | Opcode | rowspan="2" | * Adressage inhérent (registre) * Adressage immédiat (constante) | Nom de registre pour l'opérande 1 | rowspan="2" | Nom de registre pour l'opérande 2 | rowspan="2" | Nom de registre pour le résultat |- | Constante immédiate |} {|class="wikitable" |+ Encodage d'une instruction ''load-op'' |- ! rowspan="3" | Opcode | rowspan="3" | * Adressage inhérent (registre) * Adressage immédiat (constante) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source | rowspan="3" | | rowspan="3" | Nom de registre destination |- | Constante immédiate |- | Adresse mémoire ou registres pour les pointeurs |} Encoder une instruction ''load-op'' est compliqué. En soit, encoder l'opcode et les numéros de registre ne prend pas beaucoup de place, mais encoder une adresse complète prend beaucoup de bits. Une première solution utilise des instructions de taille variable : les instructions reg-reg sont plus courtes que les instructions ''load-op'', ces dernières prenant plus de place pour coder une adresse. Mais gérer des instructions de taille variable est complexe. Aussi, il existe une solution qui marche avec des instructions de taille fixe, à savoir si toutes les instructions sont codées sur le même nombre d'octets (souvent 2, 4 ou 8 octets selon le processeur) : utiliser l'encodage 2-opérandes. ==Les instructions multi-accès et leur encodage== Passons maintenant aux instructions multi-accès. Nous allons séparer les instructions d'accès mémoire d'un côté, et les instructions dyadiques de l'autre. ===Les instructions mémoire-mémoire=== Les instructions que nous allons voir dans ce qui suit sont des instructions d'accès mémoire, qui sont en plus multi-accès. Elles sont connues sous le nom d''''instruction mémoire-mémoire''', ce qui signifie qu'elle lisent une donnée en mémoire, pour l'enregistrer en mémoire. Les instructions mémoire-mémoire les plus communes sont les '''instructions de copie mémoire''', où une adresse est copiée dans une autre. Les copies en mémoire sont des opérations très fréquentes, il est très fréquent qu'un programme copie un bloc de mémoire dans un autre et beaucoup de programmeurs ont déjà été confronté à un tel cas. Aussi, les processeurs ajoutent des instructions multi-accès pour accélérer ces copies, ce qui fait un bon compromis entre performance et simplicité d'implémentation. Avec elles, la source et la destination sont systématiquement des adresses, mais, il faut préciser leur mode d'adressage : absolu, pointeur, autre. Pour préciser le mode d'adressage, il est possible de préciser un mode d’adressage différent pour la source et de la destination. Par exemple, on peut utiliser le mode d'adressage base + indice pour la source, l'adressage absolu pour la destination. {|class="wikitable" |+ Encodage d'une instruction de copie mémoire |- ! Opcode | Mode d'adressage de la source * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice * etc. | Mode d'adressage de la destination * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice * etc. | Référence(s) vers la source | Référence(s) vers la destination |} Une autre solution utilise le même mode d'adressage pour la source et la destination. On perd en flexibilité, mais ce n'est pas si grave. Les instructions de copie mémoire sont souvent utilisées pour copier des tableaux, qui sont souvent adressés avec l'adressage base + indice ou indirect à registre. Par contre, l'encodage de l'instruction est plus simple, on économise quelques bits. {|class="wikitable" |+ Encodage d'une instruction de copie mémoire |- ! Opcode | Mode d'adressage de la source et de la destination * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice | Référence(s) vers la source | Référence(s) vers la destination |} Les instructions multi-accès précédentes demandent d'encoder deux adresses dans l'instruction, ainsi que le mode d'adressage pour ces deux adresses. Les instructions sont donc assez longues et ne tiennent pas dans une instruction de taille fixe, des instructions de taille variables sont utilisées. Quelques processeurs fusionnent les instructions de copie mémoire avec les instructions LOAD, STORE et MOV. Elles ont alors une '''instruction mémoire généraliste''', souvent appelée MOV. Elle est capable de faire une lecture, une écriture, une copie entre registres et une copie d'une adresse mémoire vers une autre. Source comme destination doivent encoder le mode d'adressage adéquat, elles n'utilisent pas le même mode d'adressage. Typiquement, le premier opérande désigne la source et l'autre la destination. * Si les deux opérandes sont des registres, le premier registre est copié dans le second. * Si le premier opérande est un registre et l'autre une adresse, c'est une écriture. * Si le premier opérande est une adresse et l'autre un registre, c'est une lecture. * Si les deux opérandes sont des adresses, c'est une copie mémoire. {|class="wikitable" |+ Encodage d'une instruction mémoire généraliste |- ! rowspan="3" | Opcode | rowspan="3" | Mode d'adressage de la source * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) * Adressage immédiat (constante) | rowspan="3" | Mode d'adressage de la destination * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source | Nom de registre destination |- | Adresse mémoire ou registres pour les pointeurs | Adresse mémoire ou registres pour les pointeurs |- | Constante immédiate |} ===Les instructions multi-accès monadiques et dyadiques=== Il existe de rares instructions multi-accès monadiques. Un exemple est celui des instructions de décalage sur les CPU x86. L'opérande à décaler est lue soit depuis les registres, soit depuis la mémoire, elle est écrite au même endroit. Elle fait donc deux accès mémoire, si l'opérande est en RAM : un pour lire d'opérande, l'autre pour écrire le résultat. L'instruction fournit une seule adresse ou un seul numéro de registre, qui sert à la fois pour la source et pour la destination, pour l'opérande et le résultat. Il s'agit donc d'un encodage 1-référence, une sorte d'équivalent de l'encodage 2-référence pour les instructions monadiques. Les instructions monadiques de ce type, qui lisent une opérande en RAM et écrivent un résultat en RAM, au même endroit, porte un nom spécifique. Elles sont mine de rien assez courante. Tous les processeurs modernes en supportent quelques unes. Mais nous ne pouvons pas en parler à ce moment là du cours. La raison est que ces instructions sont toutes des instructions utilisées sur les processeurs multicœurs, dans des contextes très spécifiques, que nous n'avons pas encore abordé. Pour rentrer dans le détail, il s'agit d'une sous-classe de ces fameuses instructions atomiques appelées '''instructions ''read-modify-write'''''. Tout cela deviendra plus clair une fois qu'on en sera au chapitre sur les instructions atomiques. Les processeurs peuvent gérer quelques instructions multi-accès dyadiques. De rares instructions lisent deux opérandes en mémoire, mais le résultat est enregistré dans les registres. Par exemple, sur les processeurs x86, l'instruction de comparaison CMPS peut lire deux opérandes en mémoire RAM et les comparer. Le résultat de la comparaison est mémorisé dans le registre d'état, pas ailleurs, qui est adressé implicitement. L'instruction lit ses opérandes dans la mémoire, pas ailleurs. Enfin, il existe de rares processeurs où les opérandes peuvent être lus depuis les registres ou la RAM, idem pour l'écriture du résultat qui peut se faire dans les registres ou en RAM. De tels processeurs ont tous des instructions de taille variable. En effet, les instructions vont d'instructions qui encodent deux/trois registres, à des instructions encodant deux/trois adresses. Autant les premières sont très courtes, autant les autres sont très longues. La seule solution pratique est d'utiliser des instructions de taille variable. ==Le cas des processeurs x86== Après avoir vu la théorie, nous allons étudier l'encodage des instructions des CPU x86, présents dans nos PC. Ils ont un des encodage les plus complexe qui soit. Ils utilisent des instructions de longueur variable, qui font de 1 à 15 octets. Le jeu d'instruction contient aussi un grand nombre d'instructions, qui n'a eu de cesse d'augmenter avec le temps. Mais surtout, les instructions en question sont assez complexes. Il supporte des instructions cst-reg, reg-reg et ''load-op'', mais aussi quelques instructions multi-accès. Nous allons d'abord voir les instructions les plus complexes, histoire de voir un exemple concret d'instructions multi-accès, avant de voir comment sont encodées les instructions en général. ===L'encodage d'une instruction x86=== L'encodage d'une instruction x86 contient au minimum un opcode, qui peut-être complété avec des informations annexes. Premièrement, il peut être précédé d'un préfixe, qui complète l'opcode. Il modifie l'interprétation de l'opcode ou des octets qui suivent. Il existe un paquet de préfixes très différents dont nous ne pouvons pas encore parler ici. Il y a notamment un préfixe spécifique pour les instructions 64 bits. Suivant le préfixe, l'opcode peut passer de 1 à 3 octets. Il peut y avoir au maximum 4 préfixes, chacun codé sur un octet. Ils sont facultatifs. Deuxièmement, l'opcode peut être suivi par un octet qui précise le mode d'adressage. Il est facultatif car certaines instructions utilisent uniquement le mode d'adressage implicite. Il est appelé l'octet ModR/M. Troisièmement, l'octet ModR/M peut être suivi par trois informations, qui sont utilisées pour calculer l'adresse de l'opérande chargée depuis la mémoire. Elles regroupent un octet SIB pour l'adressage base + indice, un ''offset'', pour les adressages base + ''offset'' et/ou une constante immédiate. Les trois peuvent être combinées si le mode d'adressage des deux opérandes le permet, mais cela demande d'avoir une opérande immédiate couplée à une opérande en mode d'adressage base + indice + ''offset''. Ce n'est pas très courant. {|class="wikitable" |- ! Préfixes !! opcode !! Mod R/M !! SIB !! Displacement !! Immediate |- | Préfixes || opcode || Mode d'adressage || registres pour l'adressage base + indice || ''offset'' || Constante immédiate |- | facultatif || obligatoire || facultatif || facultatif || facultatif |- | 0 à 4 octets || 1, 2, 3 octets, souvent un seul || 1 octet || 1 octet || 1, 2 ou 4 octets || 1, 2, 4 octets en 32 bits, peut aller jusqu'à 8 en 64 bits. |} L'octet SIB inclu deux numéros de registres : un pour le registre de base, un autre pour le registre d'indice. Il inclu aussi la taille de l'opérande, qui est utilisée pour multiplier l'indice, avant de l'additionner au registre de base. {|class="wikitable" |+ Octet SIB |- ! Taille de l'opérande !! Base !! Indice |- | Entier || Numéro de registre || Numéro de registre |- | 2 bits || 3 bits || 3 bits |} L'octet Mod R/M est composé de trois champs, appelés MOD, REG et R/M. MOD est codé sur deux bits et précise le mode d'adressage : * 00 pour l'adressage indirect à registre ou l'adressage base + indice. * 01 pour dire qu'un ''offset'' de 1 octet est présent. * 10 pour dire qu'un ''offset'' de 4 octets est présent. * 11 si les deux opérandes sont dans un registre. Encoder des numéros de registres sur 3 bits limite le processeur à 8 registres. Les CPU x86 s'en sont contenté jusqu’à la génération 32 bits. Lors du passage au 64 bits, 8 autres registres ont été ajouté. Pour ajouter des registres, l'octet SIB se voit ajouter deux bits, un par numéro de registre, ce qui fait qu'il est alors codé sur 10 bits. Mais cela n'est le cas que si l'octet d'opcode est précédé par l'octet de préfixe adéquat, le préfixe REX, VEX ou XOP. Pour les opérations dyadiques, REG encode le numéro de registre de la première opérande, R/M encode le numéro de registre pour la seconde opérande. Pour les opérations à une seule opérande, le champ REG est ignoré et le numéro de registre est dans le champ R/M. En clair, l'octet Mod R/M a une interprétation qui dépend de l'opcode. De plus, la destination est encodée via encodage 2-opérande. La destination est soit la première opérande, soit la seconde. Un bit de l'opcode permet de choisir, ce qui donne un peu de flexibilité à l'encodage 2-opérande. {|class="wikitable" |+ Octet SIB |- ! Taille de l'opérande !! Base !! Indice |- | Entier || Numéro de registre || Numéro de registre |- | 2 bits || 3 bits || 3 bits |} ===Les ''string instructions''=== Les instructions dont nous allons parler sont connues sous le nom d'''instructions de chaines de caractère'', bien qu'elles travaillent en réalité plus sur des tableaux ou des zones de mémoire. Les opérations arithmétiques et logiques ne peuvent pas lire deux opérandes en mémoire, la possibilité est donc limitée à quelques instructions d'accès mémoire bien précises. Les instructions de chaine de caractère peuvent se classer en deux types : celles qui ne font qu'un seul accès mémoire, et celles qui en font deux. Les instructions de chaine de caractère multi-accès sont les suivantes : * L'instruction MOVS copie une adresse mémoire dans une autre, c'est la seule instruction de copie mémoire sur ces CPU. * L'instruction de comparaison CMPS peut lire deux opérandes en mémoire RAM et les comparer. Le résultat de la comparaison est mémorisé dans le registre d'état, pas ailleurs. Les autres instructions de chaine de caractère regroupent trois instructions : * LOD charge une donnée dans le registre EAX, l'adresse de cette donnée est dans le registre ESI. * STO copie une donnée en mémoire RAM, la donnée est dans le registre EAX, l'adresse mémoire est dans le registre ESI. * SCA charge une donnée et la compare avec le registre EAX, l'adresse de la donnée est dans le registre ESI. Pour les échanges entre deux adresses mémoire, les processeurs x86 fournissent une instruction dédiée, appelée MOVS. L'adresse de la source est dans le registre ESI, l'adresse de destination est dans le registre EDI. Vous remarquerez que les registres utilisés sont fixés à l'avance, ce qui fait qu'ils sont adressés implicitement. Les deux registres sont incrémentés/décrémentés à chaque exécution de l'instruction, afin de pointer vers le mot mémoire suivant. Fait intéressant, les instructions mémoires peuvent être répétées automatiquement plusieurs fois, en leur ajoutant un préfixe, un octet placé avant l'opcode. Le nombre de répétitions doit être stocké dans le registre ECX, qui est décrémenté à chaque exécution de l'instruction. Une fois que le registre ECX atteint 0, l'instruction n'est plus répétée et on passe à l'instruction suivante. En clair, l'instruction décrémente automatiquement ce registre à chaque exécution et s'arrête quand il atteint zéro. Suivant le préfixe, d'autres conditions peuvent être ajoutées. Elle peut être répétée plusieurs fois avec l'ajout du préfixe REP. L'instruction avec préfixe, REPMOVS, permet donc de copier un bloc de mémoire dans un autre, de copier N adresses consécutives ! Cela permet de parcourir la mémoire dans l'ordre montant (adresses croissantes) ou descendant (adresses décroissantes), suivant que les registres sont incrémentés ou décrémentés. La direction de parcours de la mémoire est spécifiée dans un bit incorporé dans le processeur, qui vaut 0 (décrémentation) ou 1 (incrémentation), et peut être altéré par des instructions dédiées, de manière à être mis à 1 ou 0. D'autres instructions d'accès mémoire peuvent être préfixées avec REP. Par exemple, l'instruction STOS, qui copie le contenu du registre EAX dans une adresse. On peut exécuter cette instruction une fois, ou la répéter plusieurs fois avec le préfixe REP. Là encore, on doit préciser l'adresse à laquelle écrire dans un registre spécifié, puis le nombre de répétitions dans le registre ECX. Là encore, le nombre de répétitions est décrémenté à chaque exécution, alors que l'adresse est incrémentée/décrémentée. L'instruction REPSTOS est très utile pour mettre à zéro une zone de mémoire, ou pour initialiser un tableau avec des données préétablies. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les modes d'adressage | prevText=Les modes d'adressage | next=Les jeux d'instructions | nextText=Les jeux d'instructions }} </noinclude> 0a213jfg9d2n3roufs7n7rrltcd9rf0 773856 773855 2026-10-01T13:36:24Z Mewtow 31375 /* L'encodage des instructions est avant tout un compromis */ 773856 wikitext text/x-wiki Pour rappel, les programmes informatiques sont placés en mémoire, au même titre que les données qu'ils manipulent. En clair, les instructions sont stockées dans la mémoire RAM/ROM sous la forme de suites de bits, tout comme les données. Il est impossible de faire la différence entre donnée et instruction, vu que rien ne ressemble plus à une suite de bits qu'une autre suite de bits. La seule différence est que les instructions sont chargées via le ''program counter'', alors que les données sont lues ou écrites par des instructions d'accès mémoire. En théorie, cette différence fait que le processeur ne peut pas prendre par erreur des instructions pour des données. ==La taille d'une instruction : taille fixe ou variable== Une instruction est codée sur plusieurs bits. Le nombre de bits utilisé pour coder une instruction est appelée la '''taille de l'instruction'''. Sur certains processeurs, la taille d'une instruction est fixe, c’est-à-dire qu'elle est la même pour toutes les instructions. Mais sur d'autres processeurs, les instructions n'ont pas toutes la même taille, ils gèrent des instructions de longueur variable. Un exemple de jeu d’instruction à longueur variable est le x86 des pc actuels, où une instruction peut faire entre 1 et 15 octets. L'encodage des instructions x86 est tellement compliqué qu'il prendrait à lui seul plusieurs chapitres ! Les instructions de longueur variable permettent d'économiser un peu de mémoire : avoir des instructions qui font entre 1 et 3 octets est plus avantageux que de tout mettre sur 3 octets. Mais en contrepartie le chargement de l'instruction suivante par le processeur est rendu plus compliqué. Le processeur doit en effet identifier la longueur de l'instruction courante pour savoir où est la suivante. À l'opposé, des instructions de taille fixe gâchent un peu de mémoire, mais permettent au processeur de calculer plus facilement l’adresse de l'instruction suivante et de la charger plus facilement. Il existe des processeurs qui sont un peu entre les deux, avec des instructions de taille fixe, mais qui ne font pas toutes la même taille. Un exemple est jeu d'instruction RISC-V, où il existe des instructions "normales" de 32 bits et des instructions "compressées" de 16 bits. Le processeur charge un mot de 32 bits, ce qui fait qu'il peut lire entre une et deux instructions à la fois. Au tout début de l'instruction, un bit est mis à 0 ou 1 selon que l'instruction soit longue ou courte. Le reste de l'instruction varie suivant sa longueur. ==L'encodage d'une instruction : généralités== Il est intéressant d'étudier comment les instructions sont encodées, à savoir comment sont organisés ses bits. En effet, une instruction n'est pas qu'une suite de bits sans signification. Elle est en fait découpée en '''champs''', à savoir des suites de bits qui ont une signification. ===L'opcode, aussi appelé "code opération"=== Toute instruction contient un champ opcode, qui indique quelle est l'instruction : est-ce une instruction d'addition, de soustraction, un branchement inconditionnel, un appel de fonction, une lecture en mémoire, etc. Le terme opcode est une abréviation pour "code opération". Typiquement, l'opcode est encodé sur un octet, voire moins. Plus l'opcode est encodé sur un grand nombre de bits, plus le processeur peut supporter un grand nombre d'instruction. Par exemple, un octet d'opcode permet d'encoder 256 instructions différentes, 5 bits d'opcode seulement 32 instructions, etc. Pour la même instruction, l'opcode peut être différent suivant le processeur, ce qui est source d'incompatibilités. Par exemple, les opcodes de l'instruction d'addition ne sont pas les mêmes sur les processeurs x86 (ceux de nos PC) et les anciens macintosh, ou encore les microcontrôleurs. Ce qui fait qu'un opcode de processeur x86 n'aura pas d'équivalent sur un autre processeur, ou correspondra à une instruction totalement différente. En théorie, l'opcode a une taille fixe, ce qui veut dire qu'il est toujours codé sur le même nombre de bits, pour toutes les instructions. Par exemple, sur les processeurs ARM7, l'opcode est encodé sur 8 bits. Il n'y a pas d'instruction qui a un opcode de 5 bits, une autre qui a un opcode de 8 bits, etc. Non, toutes les instructions ont un opcode de 8 bits. Mais il y a des jeux d'instruction qui font exception. Par exemple, sur les processeurs x86, l'opcode peut faire 1, 2 ou 3 octets. Le mécanisme pour gérer des opcodes de taille variable sera expliqué plus tard dans ce chapitre. Il arrive que de rares instructions ne soient composées d'un opcode, sans rien d'autre. Elles ont alors une représentation en binaire qui est unique. : Pour l'anecdote, certains processeurs n'utilisent qu'une seule et unique instruction, et qui peuvent se passer d'opcodes. Mais ce sont des curiosités, pas quelque chose d'utilisé en pratique. ===L'encodage des opérandes et des références=== En plus de l'opcode, une instruction doit préciser où sont ses opérandes. Par opérande, on veut parler des données manipulées par l'instruction. Pour cela, il y a deux cas possibles. * Le premier encode l'opérande dans l'instruction, avec l'adressage immédiat. L'opérande est alors une constante connue à la compilation, elle est intégrée dans l'instruction directement, après l'opcode. * Le second cas indique où se trouve l'opérande, en donnant une référence vers celle-ci. La référence précise dans quel registre ou à quelle adresse se trouve l'opérande. La référence a un champ dédié, rien que pour elle, à la suite de l'opcode. La référence est associée à un mode d'adressage qui précise comment l'interpréter : si c'est un numéro de registre, une adresse mémoire, un pointeur, de quoi calculer l'adresse, etc. Mais laissons de côté le mode d'adressage pour le moment et concentrons-nous sur l'encodage de l'opérande ou de sa référence. Les références n'ont pas toutes la même taille : une adresse utilise plus de bits qu'un numéro de registre, par exemple. En conséquence, une instruction dont les deux références sont des adresses mémoire prendra plus de place qu'un calcul entre deux registres. Les constantes immédiates ont souvent une taille proche de celle des adresses. En effet, les adresses mémoire sont un peu l'équivalent des constantes immédiates, mais pour les instructions d'accès mémoire. Un problème est que les instructions n'ont pas le même nombre d'opérandes. Les instructions arithmétiques et logiques sont souvent des '''instructions dyadiques''', à savoir qui prennent deux opérandes et fournissent un résultat. L'addition, la soustraction, les opérations bit à bit ET/OU/XOR, les décalages et rotations, sont dans ce cas. Il existe cependant quelques rares '''instructions monadiques''' qui n'ont qu'une seule opérande et fournissent un résultat, comme les instructions NOT, INC/DEC (incrémentation/décrémentation). Vu qu'elles n'ont pas le même nombre d'opérande, le nombre de bits utilisé pour encoder l'instruction ne sera pas le même. Ne parlons pas des opérations d'accès mémoire qui n'ont ni opérande ni résultat. Elles copient une donnée d'une source vers une destination. Par exemple, l'instruction MOV copie une donnée d'un registre source vers un registre destination, l'instruction LOAD charge une donnée depuis une adresse source vers un registre destination, etc. Et la source comme la destination doivent être encodés dans l'instruction, ce qui est équivalent à encoder deux opérandes. Les instructions de longueur variables permettent de résoudre ce problème de longueur. Une instruction de longueur variable prend autant de bits qu'elle a besoin pour encoder ses opérandes et son résultat, ou pour encoder sa source et de sa destination. Une instruction dyadique prendra donc plus de place qu'une instruction monadique. Mais les instructions de longueur fixe se débrouillent autrement. Elles se calent sur le pire des cas, l'instruction la plus longue, et doivent trouver un moyen de tout faire rentrer dans 16, 32 ou 64 bits. Généralement, elles réduisent les possibilités d'adressage, certaines opérandes sont encodées de manière implicite, on empêche certaines combinaisons d'opérandes, etc. Leur encodage est donc soit plus compliqué, soit plus restrictif. Une solution souvent utilisée réduit la taille des opérations dyadiques. Intuitivement, encoder une opération dyadique demande d'encoder trois références : une par opérande, plus une pour le résultat. Le résultat est ce qu'on appelle à tord l'encodage 3-opérandes, en confondant le résultat avec une opérande. Une autre dénomination tout aussi mauvaise est l'encodage encodage 3-adresses car une telle instruction encode 3 adresses, trois noms de registre, etc. Nous parlerons plutôt d''''encodage 3-références''', plus correct, qui indique bien que l’instruction encode trois références. Ce n'est pas parfait car l'une de ces opérandes peut très bien être une constante immédiate, mais passons. Mais encoder 2 opérandes + 1 résultat prend de la place. L'encodage 3-référence peut être modifié de manière à adresser le résultat de manière implicite. Pour économiser un peu de place, l'idée est d'adresser le résultat de manière implicite. Avec cet encodage, le résultat est enregistré dans le registre de la seconde opérande. Pour le dire autrement, l'opérande sera remplacé par le résultat de l'instruction, l'opérande est écrasé. Avec cet encodage, les instructions ne précisent que deux opérandes, pas le résultat, ce qui permet de gagner de la place. On parle alors d''''encodage 2-références''', d'encodage 2-adresses, ou encore d'encodage 2-opérandes. Un exemple d'utilisation est celui des processeurs RISC-V, qu'on a mentionné plus haut. Nous avions dit qu'ils géraient des instructions 32 bits, et des instructions compressées de 16 bits. Les instructions de 32 bits sont des instructions à trois adresses : elles indiquent deux opérandes et la destination du résultat. Les instructions de 16 bits n'ont que deux opérandes et encodent le résultat de manière implicite. Cela explique qu'elles soient plus courtes : deux opérandes prennent moins de place que trois. L'encodage 2-références peut aussi être utilisé avec l'adressage immédiat. Une instruction peut l'utiliser si un opérande est en adressage immédiat, les deux autres sont dans des registres. La raison est qu'une constante prend approximativement autant de bits qu'une adresse. Les deux sont souvent codées sur 16 bits, plus rarement 20/24 bits. Avec des instructions codées sur 32 bits, cela ne laisse qu'un ou deux octets pour coder l'opcode et les registres. L'encodage 2-opérande est alors une nécessité, économiser un numéro de registre est le seul moyen d'avoir assez de place si la constante est codée sur 20/24 bits. Les instructions monadiques utilisent pas défaut l'encodage 2-références, pour préciser l'opérande et le résultat. Mais elles peuvent aussi utiliser l'encodage une-référence, qui est aux instructions monadiques ce qu'est l'encodage 2-référence aux instructions dyadiques. L'idée est que le résultat écrase l'opérande, il la remplace. Le résultat est écrit à la même adresse que l'opérande, ou il est écrit dans le même registre. L'avantage est qu'on a besoin de ne préciser qu'une seule opérande, pas deux. L'instruction est donc plus courte. [[File:CODE2 formatos.jpg|centre|vignette|upright=2|Exemples d'encodage d'instruction sur une taille fixe. </br> 0 - Opération sans opérande. </br> 1 - Instruction mono-opérande en encodage 1-référence. </br> 2 - Instruction de branchement. </br> 3 - Instruction dyadique en encodage 2-opérande. </br> 4 - Instruction dyadique en encodage 3-opérandes.]] ===L'encodage du mode d'adressage=== Il existe deux méthodes principales pour préciser le mode d'adressage utilisé par l'instruction, avec un cas intermédiaire. Dans le premier cas, l'instruction ne gère qu'un mode d'adressage par opérande. Par exemple, prenons le cas d'une instruction COPY qui copie un registre dans un autre. sur les processeurs dit RISC (''Reduced Instruction Set Computer'') , les instructions arithmétiques ne peuvent manipuler que des registres. Un autre cas est celui de l'instruction MOV, qui peut copier un registre ou une constante dans un registre destination. La destination est forcément un registre, pas besoin d'encoder le mode d'adressage pour le registre destination. Par contre, la source doit être encodée explicitement. Dans des cas pareils, le mode d'adressage est déduit automatiquement via l'opcode : on parle de '''mode d'adressage implicite'''. Dans un cas intermédiaire, il se peut que plusieurs instructions existent pour faire la même chose, mais avec des modes d'adressages différents. Le mode d'adressage est alors encodé dans l'opcode, ou en est déduit. Il s'agit d'un cas particulier d'adressage implicite. Un exemple est celui des processeurs ARM, qui ont plusieurs instructions d'addition. Une qui ne manipule que des registres, et adresse trois registres, avec mode adressage implicite. Une autre additionne une constante immédiate avec un registre, ce qui fait que la constante immédiate et les deux registres source/destination sont aussi à mode d'adressage implicite. [[File:Mips32 addi.svg|centre|vignette|upright=2|Exemple d'une instruction avec mode d'adressage implicite, appartenant au jeu d'instruction MIPS.]] Dans le second cas, les instructions gèrent plusieurs modes d'adressage par opérande. Par exemple, une instruction d'addition peut additionner soit deux registres, soit un registre et une adresse, soit un registre et une constante. Dans un cas pareil, l'instruction doit préciser le mode d'adressage utilisé, au moyen de quelques bits intercalés entre l'opcode et les opérandes. On parle de '''mode d'adressage explicite'''. Sur certains processeurs, chaque instruction peut utiliser tous les modes d'adressage supportés par le processeur : on dit que le processeur est orthogonal. [[File:Machine instructions.svg|centre|vignette|upright=2|Exemple d'une instruction avec mode d'adressage explicite.]] Maintenant, tout cela ne vaut que pour encoder le mode d'adressage d'une donnée, pas plus. Mais une instruction gère plusieurs données : ses opérandes, son résultat. Pour les accès mémoire, c'est sa source et de destination. Il faut donc préciser plusieurs modes d'adressage : un par opérande, plus un pour le résultat. Les instructions manipulant plusieurs opérandes peuvent parfois utiliser un mode d'adressage différent pour chaque opérande. Par exemple, une addition manipule deux opérandes, ce qui demande d'utiliser un mode d'adressage par opérande (dans le pire des cas). Il faut donc préciser plusieurs modes d'adressage : un par opérande, plus un pour le résultat. Intuitivement, on se dit qu'il faut utiliser un encodage explicite pour chacune d'entre elles. Idem pour le résultat. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande/résultat est adressée explicitement |- ! Opcode ! Mode d'adressage opérande 1 | Référence vers l'opérande 1 ! Mode d'adressage opérande 2 | Référence vers l'opérande 2 ! Mode d'adressage résultat | Référence vers le résultat |} L'encodage du mode d'adressage prend quelques bits, entre 1 et 4, pas plus. Il est donc intéressant de regrouper les modes d'adressage dans un même octet, dans un même champ. Faire ainsi sépare bien les références/opérandes d'un côté, et les bits de contrôle de l'autre, qui encodent l'instruction proprement dite. Et cela se marie très bien avec les instructions de longueur variable. Elles gardent un cœur de taille fixe auquel on colle des opérandes/références de taille variable. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande/résultat est adressée explicitement |- ! Opcode ! Mode d'adressage opérande 1 ! Mode d'adressage opérande 2 ! Mode d'adressage résultat | Référence vers l'opérande 1 | Référence vers l'opérande 2 | Référence vers le résultat |} Avec l'encodage 2-référence, on n'a pas à encoder explicitement la référence du résultat, ni son mode d'adressage. Cela fait gagner un peu de place. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande est adressée explicitement, le résultat est adressé implicitement |- ! Opcode ! Mode d'adressage opérande 1 ! Mode d'adressage opérande 2/résultat | Référence vers l'opérande 1 | Référence vers l'opérande 2 / le résultat |} Il s'agit là d'un usage de l'encodage implicite du mode d'adressage. Mais il y en a d'autres, comme nous le verrons dans ce qui suit. ===L'encodage de la taille des opérandes=== Enfin, une instruction encode la taille des données qu'elle manipule. C'est surtout utile pour les instructions d'accès mémoire. Il est en théorie possible de se limiter à des instructions mémoire qui lisent/écrivent des données de la même taille que les registres entiers, souvent égal à la taille d'un mot mémoire. Mais dans les faits, seules les architectures anciennes à adressage par mot font ça. De nos jours, les architectures à adressage par byte permettent de préciser qu'on veut lire un entier de 8 bits, 16 bits, 32 bits, 64 bits. Pour cela, la solution la plus simple est d'encoder la taille des données dans l'instruction. On rajoute un champ qui indique quelle est la taille des données à lire/écrire. Le champ n'encode pas un nombre comme 8, 16, 32, 64. À la place, il contient deux bits qui sont à interpréter comme suit : * 00 pour des données de 8 bits ; * 01 pour des données de 16 bits ; * 10 pour des données de 32 bits ; * 11 pour des données de 64 bits. En général, ce champ n'existe que pour les instructions d'accès mémoire, qui sont les seules qui se préoccupent de la taille des opérandes. La taille des données est surtout pertinente quand on lit/écrit en mémoire RAM/ROM. Les opérations arithmétiques sont rarement concernées, car elles manipulent le plus souvent des registres, qui n'ont qu'une seule taille. Il y a des exemples où c'est le cas, comme avec le système d'''alias'' de registres des processeurs x86, mais c'est l'exception plus que la règle. Et de plus, ça ne sert que très rarement de limiter la taille des résultats, surtout qu'une simple opération de masquage peut toujours être employée pour éliminer les bits de poids fort. Quelques jeux d'instructions ajoutent ce système de taille des opérandes pour les opérations arithmétiques simples, comme les additions et soustractions. Il arrive même que certaines opérations gèrent la taille des opérandes, mais pas d'autres, sur certaines jeux d'instruction irréguliers. La raison est une question de compatibilité, par exemple quand un jeu d'instruction de 16 bits a été étendu au 32 bits. Les anciennes instructions gardent leur encodage et travaillent sur 16 bits, d'autres sont ajoutées pour gérer les opérandes 32 bits. Les processeurs x86 utilisent un système totalement différent, avec un pseudo-aliasing des registres, qu'on a vu dans le chapitre sur les registres. ===L'encodage des instructions est avant tout un compromis=== Encoder des instructions est quelque chose d'assez complexe, qui demande de faire un compromis entre des contraintes opposées, contradictoires. Pour savoir ce que doit faire un encodage idéal, faisons une petite liste. Idéalement, un encodage doit utiliser des instructions de taille fixe, afin de simplifier le chargement/décodage des instructions. Avec des instructions de taille variable, le processeur a plus de mal à déterminer où se trouve la prochaine instruction. Il doit déterminer la longueur de l'instruction pour savoir où se trouve la suivante. Alors qu'avec des instructions de taille fixe, le calcul est nettement plus simple, la longueur de l'instruction est connue à l'avance. Les instructions doivent aussi avoir une taille qui est une puissance de deux, afin de simplifier l'alignement des instructions en mémoire. Les instructions peuvent donc faire 8, 16, 32, 64 bits. Et là, voyons chaque taille au cas par cas. * Encoder des instructions sur 8 bits est juste impossible, il n'y a pas assez de bits. Sauf sur des jeux d'instructions spécialisés appelés des machines à pile, qui auront droit à un chapitre dédié. * Encoder des instructions sur 16 bits, c'est possible, mais il faut oublier l'usage de constantes immédiates ou l'adressage absolu, les constantes et adresses ne rentrant pas dans une instruction aussi petite. Concrètement, il faut utiliser des jeux d'instructions spéciaux, comme des architectures à accumulateur, qu'on verra dans quelques chapitres. * Encoder des instructions sur 32 bits est la règle, tous les processeurs font cela de nos jours. Il y a assez de place pour encoder des constantes immédiates et des adresses assez larges, disons 16 ou 24 bits. De même, encoder des instructions trois-opérandes utilisant des registres n'est pas un problème : 32 bits est suffisant pour encoder 4 registres et un opcode. * Encoder des instructions sur 64 bits serait complétement excessif. On aurait assez de place pour mettre plein d'instructions, pleins de registres, des constantes et adresses très larges. Par contre, doubler la taille des instructions signifie doubler la taille du code, les programmes prennent deux fois plus de place qu'avec des instructions 32 bits. Et il faut aussi deux fois plus de débit binaire pour charger le même nombre d'instructions, ce qui implique de doubler la taille du bus qui relie le processeur à la mémoire des instructions. Pour résumer, des instructions de 32 bits sont l'idéal : assez longues pour qu'on puisse encoder des instructions utiles, pas assez longue pour pose problème niveau matériel/mémoire. Il faut un peu pousser pour que ca rentre, si j'ose dire, mais rien d'infaisable. ==Les restrictions sur les modes d'adressage== Intuitivement, on se dit que toute opérande peut utiliser tous les modes d'adressage possibles, il n'y a pas de restrictions particulières. Cependant, ce n'est pas le cas du tout. Permettre à toutes les opérandes d'utiliser tous les modes d'adressage possibles pose quelques problèmes, qu'on ne peut pas résoudre facilement. ===La gestion du mode d'adressage immédiat=== Le premier problème est que de nombreuses combinaisons sont interdites, voire n'ont aucun sens. Par exemple, ça n'aurait aucun sens d'utiliser l'adressage immédiat pour le résultat d'une instruction. De même, ça n'aurait aucun intérêt d'utiliser le mode d'adressage immédiat pour les deux opérandes d'une opération dyadique. Concrètement, le mode d'adressage immédiat ne peut être utilisé que sur un opérande à la fois. De plus, l'adressage immédiat n'a de sens que pour les opérations dyadiques, mais n'a aucun sens avec les opérations monadiques. Une opération sur une constante donnera toujours le même résultat, autant éliminer l'instruction et précalculer ce résultat. En somme, seules les instructions dyadiques peuvent encoder une constante immédiate, pas les autres. Les instructions d'accès mémoire peuvent utiliser une variante de l'adressage immédiat : l'adressage absolu qui encode une adresse directement dans l'instruction. Mais l'encodage est assez similaire. ===Les restrictions de registres source/destination=== Les anciens processeurs avaient des restrictions quant à l'utilisation des registres. Par exemple, certaines instructions ne pouvaient utiliser que certains registres, pas les autres. D'autres avaient un registre de destination prévu à l'avance, d'autres ne pouvaient lire leurs opérandes que dans des registres précis, etc. Voyons cela avec quelques exemples. Les contraintes peuvent porter sur le registre pour le résultat. Un exemple est celui des anciennes architectures MIPS, qui limitaient le registre de résultat pour la multiplication. Les multiplications pouvaient lire leurs opérandes dans tous les registres, mais leur résultat allait dans un registre dédié, séparé des registres généraux, appelé HO/LO. Le registre était en réalité composé de deux registres : le registre LO pour les 32 bits de poids faible du résultat, le registre HO pour les 32 bits de poids fort. Les contraintes peuvent aussi porter sur les opérandes. Elles ne peuvent alors provenir que de certains registres, pas des autres. Et les restrictions peuvent porter sur les deux opérandes, ou alors uniquement sur la première opérande, ou que sur la seconde. Pour donner un exemple, prenons l'exemple des processeurs x86, avec les instructions de décalage. Elle prennent deux opérandes : l'opérande à décaler et le nombre de rangs par lequel il faut décaler. Le nombre de rangs est soit une constante immédiate, soit le registre CL. Les autres registres ne peuvent pas être utilisés pour préciser de combien il faut décaler. La même architecture avait des limitations similaires sur les très anciens processeurs x86, avant l'arrivée du CPU 386. Les opérations de multiplication et d'extension de signe devaient avoir leur première opérande dans le registre AX. De telles contraintes visent à réduire la taille des instructions. Pour reprendre le cas de l'instruction SHL, limiter le nombre de registre opérande à un seul fait qu'on peut l'adresser implicitement. ON n'a pas à encoder le numéro de registre, ce qui économise pas mal de bits. Cela permet à l'instruction de tenir sur un seul octet, deux si l'instruction utilise une constante immédiate. Un autre exemple de contrainte est celui de l'ancien processeur Data General Nova. Il disposait de 4 registres généraux (appelés improprement accumulateurs), mais seul deux d'entre eux servaient pour l'adressage mémoire. Précisément, le processeur gérait trois modes d'adressage, dont l'adressage absolu indicé. Pour rappel, celui-ci additionne une adresse fixe avec un indice variable. L'adresse est intégrée à l'instruction, alors que l'indice est dans un registre. Et sur le Data General Nova, l'indice ne pouvait être que dans les deux derniers registres, pas les deux premiers. ===Les restrictions sur les opérandes mémoire=== Une seconde restriction est liée à la source des opérandes. Un opérande peut provenir de deux sources : des registres, de la mémoire RAM, ou être fournie via adressage immédiat. Le résultat peut lui être enregistré en RAM ou dans les registres. Et cela met en avant un second problème : en faisant cela, une instruction peut effectuer plusieurs accès mémoire. Par exemple, il est possible de lire deux opérandes en mémoire RAM et d'enregistrer le résultat en mémoire RAM. Ou encore de lire deux opérandes en RAM et d'enregistrer le résultat dans les registres. Les processeurs gèrent ce problème de deux manières différentes. La première interdit de faire plusieurs accès mémoire par instruction, ce qui impose des limitations quant à l'utilisation des modes d'adressage. Typiquement, une instruction peut lire un opérande en RAM, mais pas plus. Le jeu d'instruction interdit certaines combinaisons de modes d'adressage avec deux opérandes. La seconde méthode autorise des instructions à faire plusieurs accès mémoire par instruction. Par exemple, une opération dyadique peut lire deux opérandes en RAM, copier une donnée d'une adresse mémoire à une autre, etc. Les modes d'adressages sont moins limités, des combinaisons deviennent possibles. Dans ce qui va suivre, nous allons séparer les instructions en plusieurs types : celles qui ne font pas d'accès mémoire, celles qui en font un, celles qui en font plusieurs. Nous parlerons d''''instruction simple-accès''' pour celles qui font au maximum un accès mémoire. Les instructions qui effectuent plusieurs accès mémoire seront appelées des '''instructions multi-accès'''. Et nous allons les voir séparément. ==L'encodage des instructions simple-accès== Pour commencer, nous allons voir l'encodage des instructions sur les processeurs qui ne gèrent pas plusieurs accès mémoire par instruction. Leur encodage est plus compact que pour les instructions multi-accès. ===Les instructions mémoire simple-accès et leur encodage=== Sur la plupart des processeurs, les instructions ne peuvent faire qu'un seul accès mémoire. Il est interdit de faire plusieurs accès mémoires par instruction. La première conséquence est que les seules instructions d'accès mémoire permises sont : * les instructions LOAD pour charger une donnée de la RAM dans un registre ; * l'instruction STORE pour copier une donnée d'un registre vers la RAM ; * l'instruction MOV en adressage inhérent (registre), qui copie un registre dans un autre ; * l'instruction MOV en adressage immédiat (constante) pour charger une constante immédiate dans un registre destination. D'autres instructions sont possibles, mais elles sont rarement implémentées. Par exemple, on peut citer l'instruction d'échange entre registres XCHG. Une autre possibilité est une variante de l'instruction STORE en mode d'adressage immédiat. Elle copie une constante dans une adresse mémoire. Elle n'est pas présente sur les architectures ARM, POWER PC ou SPARC. Mais laissons-les de côté. Pour rappel, les instructions d'accès mémoire copient une donnée d'une source vers une destination, les deux ayant leur propre mode d'adressage. Il n'est pas systématiquement encodé. Par exemple, l'instruction LOAD charge une donnée dans un registre destination, l'adressage de la destination est donc toujours l'adressage inhérent (nom de registre). Pas besoin d'encoder le mode d'adressage, juste la référence, le nom de registre. Idem pour les autres instructions. {|class="wikitable" |- ! colspan="7" | Instruction LOAD |- | Opcode | Adressage de la source * Adressage absolu (adresse) * Adressage indirect à registre * Adressage base + indice * etc. | Adresse ou référence de la source | | Nom de registre de destination |- ! colspan="7" | Instruction STORE |- | Opcode | | Nom de registre source | Adressage de la destination * Adressage absolu (adresse) * Adressage indirect à registre * Adressage base + indice * etc. | Adresse ou référence de destination |- ! colspan="7" | Instruction MOV |- | rowspan="2" | Opcode | rowspan="2" | * Adressage inhérent (registre) * Adressage immédiat (constante) | Nom de registre source | rowspan="2" | | rowspan="2" | Nom de registre destination |- | Constante immédiate |} Les processeurs x86, et sans doute quelques autres, fusionnent les instructions LOAD, STORE et MOV en une seule instruction appelée MOV. Elle est capable de faire une lecture, une écriture et une copie entre registres. Par contre, elle n'est pas capable de faire une copie d'une adresse mémoire vers une autre. Elles encodent une source et une destination, dont au moins l'une d'entre elle est un registre. Leur encodage place le registre juste après l'opcode, et la seconde opérande après. Un bit de l'instruction dit si la première opérande est la source ou la destination. {|class="wikitable" |+ Encodage d'une instruction MOV généraliste |- | rowspan="3" | Opcode | rowspan="3" | Bit qui indique l'ordre des champs | rowspan="3" | Nom de registre source/destination | rowspan="3" | * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source/destination |- | Adresse mémoire ou registres pour les pointeurs |- | Constante immédiate |} ===Les instructions dyadiques simple-accès et leur encodage=== Les instructions arithmétiques et logique sont elles aussi concernées par la contrainte d'un accès mémoire par instruction. Pour ces opérations, il y a deux possibilités : lire un opérande en RAM, enregistrer le résultat en RAM. Les deux options peuvent s'émuler avec deux instructions consécutives. La première option remplace une instruction LOAD suivie d'une instruction arithmétique/logique, la seconde remplace une instruction arithmétique/logique suivie d'une instruction STORE. Dans les deux cas, on économise un registre : le registre pour charger l'opérande dans le premier cas, le registre pour le résultat dans le second cas. Dans les faits, la seconde option n'est jamais appliquée, le résultat est systématiquement stocké dans un registre. En effet, il est fréquent qu'un opérande soit chargé depuis la mémoire, alors qu'il est rare qu'un résultat soit écrit en mémoire après avoir été calculé. Un résultat calculé a de fortes chances d'être réutilisé par la suite, donc de servir d'opérandes sous peu. Mieux vaut le laisser dans les registres, plutôt que de payer le prix d'un accès mémoire. Lire un opérande en mémoire RAM est par contre beaucoup plus fréquent et donc intéressant. [[File:Isregmem.png|vignette|Instruction dyadique de type ''load-op''.]] Dans ce qui suit, nous parlerons d''''instructions ''load-op''''' pour désigner de telles instructions. Elles lisent un opérande en RAM, mais l'autre opérande est lu depuis les registres et le résultat est enregistré dans les registres. Elles sont opposées aux '''instructions reg-reg''', ce qui est une abréviation pour registre-registre. Ces dernières lisent leurs deux opérandes dans les registres, idem pour l'écriture du résultat. Il faut aussi citer les '''instructions cst-reg''', où un opérande est fourni via adressage immédiat, l'autre opérande est dans les registres. Avec la contrainte d'un seul accès RAM par instruction, ce sont les seuls types d'instruction qui existent. {|class="wikitable" |+ Encodage d'une instruction dyadique reg-reg |- ! rowspan="2" | Opcode | rowspan="2" | * Adressage inhérent (registre) * Adressage immédiat (constante) | Nom de registre pour l'opérande 1 | rowspan="2" | Nom de registre pour l'opérande 2 | rowspan="2" | Nom de registre pour le résultat |- | Constante immédiate |} {|class="wikitable" |+ Encodage d'une instruction ''load-op'' |- ! rowspan="3" | Opcode | rowspan="3" | * Adressage inhérent (registre) * Adressage immédiat (constante) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source | rowspan="3" | | rowspan="3" | Nom de registre destination |- | Constante immédiate |- | Adresse mémoire ou registres pour les pointeurs |} Encoder une instruction ''load-op'' est compliqué. En soit, encoder l'opcode et les numéros de registre ne prend pas beaucoup de place, mais encoder une adresse complète prend beaucoup de bits. Une première solution utilise des instructions de taille variable : les instructions reg-reg sont plus courtes que les instructions ''load-op'', ces dernières prenant plus de place pour coder une adresse. Mais gérer des instructions de taille variable est complexe. Aussi, il existe une solution qui marche avec des instructions de taille fixe, à savoir si toutes les instructions sont codées sur le même nombre d'octets (souvent 2, 4 ou 8 octets selon le processeur) : utiliser l'encodage 2-opérandes. ==Les instructions multi-accès et leur encodage== Passons maintenant aux instructions multi-accès. Nous allons séparer les instructions d'accès mémoire d'un côté, et les instructions dyadiques de l'autre. ===Les instructions mémoire-mémoire=== Les instructions que nous allons voir dans ce qui suit sont des instructions d'accès mémoire, qui sont en plus multi-accès. Elles sont connues sous le nom d''''instruction mémoire-mémoire''', ce qui signifie qu'elle lisent une donnée en mémoire, pour l'enregistrer en mémoire. Les instructions mémoire-mémoire les plus communes sont les '''instructions de copie mémoire''', où une adresse est copiée dans une autre. Les copies en mémoire sont des opérations très fréquentes, il est très fréquent qu'un programme copie un bloc de mémoire dans un autre et beaucoup de programmeurs ont déjà été confronté à un tel cas. Aussi, les processeurs ajoutent des instructions multi-accès pour accélérer ces copies, ce qui fait un bon compromis entre performance et simplicité d'implémentation. Avec elles, la source et la destination sont systématiquement des adresses, mais, il faut préciser leur mode d'adressage : absolu, pointeur, autre. Pour préciser le mode d'adressage, il est possible de préciser un mode d’adressage différent pour la source et de la destination. Par exemple, on peut utiliser le mode d'adressage base + indice pour la source, l'adressage absolu pour la destination. {|class="wikitable" |+ Encodage d'une instruction de copie mémoire |- ! Opcode | Mode d'adressage de la source * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice * etc. | Mode d'adressage de la destination * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice * etc. | Référence(s) vers la source | Référence(s) vers la destination |} Une autre solution utilise le même mode d'adressage pour la source et la destination. On perd en flexibilité, mais ce n'est pas si grave. Les instructions de copie mémoire sont souvent utilisées pour copier des tableaux, qui sont souvent adressés avec l'adressage base + indice ou indirect à registre. Par contre, l'encodage de l'instruction est plus simple, on économise quelques bits. {|class="wikitable" |+ Encodage d'une instruction de copie mémoire |- ! Opcode | Mode d'adressage de la source et de la destination * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice | Référence(s) vers la source | Référence(s) vers la destination |} Les instructions multi-accès précédentes demandent d'encoder deux adresses dans l'instruction, ainsi que le mode d'adressage pour ces deux adresses. Les instructions sont donc assez longues et ne tiennent pas dans une instruction de taille fixe, des instructions de taille variables sont utilisées. Quelques processeurs fusionnent les instructions de copie mémoire avec les instructions LOAD, STORE et MOV. Elles ont alors une '''instruction mémoire généraliste''', souvent appelée MOV. Elle est capable de faire une lecture, une écriture, une copie entre registres et une copie d'une adresse mémoire vers une autre. Source comme destination doivent encoder le mode d'adressage adéquat, elles n'utilisent pas le même mode d'adressage. Typiquement, le premier opérande désigne la source et l'autre la destination. * Si les deux opérandes sont des registres, le premier registre est copié dans le second. * Si le premier opérande est un registre et l'autre une adresse, c'est une écriture. * Si le premier opérande est une adresse et l'autre un registre, c'est une lecture. * Si les deux opérandes sont des adresses, c'est une copie mémoire. {|class="wikitable" |+ Encodage d'une instruction mémoire généraliste |- ! rowspan="3" | Opcode | rowspan="3" | Mode d'adressage de la source * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) * Adressage immédiat (constante) | rowspan="3" | Mode d'adressage de la destination * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source | Nom de registre destination |- | Adresse mémoire ou registres pour les pointeurs | Adresse mémoire ou registres pour les pointeurs |- | Constante immédiate |} ===Les instructions multi-accès monadiques et dyadiques=== Il existe de rares instructions multi-accès monadiques. Un exemple est celui des instructions de décalage sur les CPU x86. L'opérande à décaler est lue soit depuis les registres, soit depuis la mémoire, elle est écrite au même endroit. Elle fait donc deux accès mémoire, si l'opérande est en RAM : un pour lire d'opérande, l'autre pour écrire le résultat. L'instruction fournit une seule adresse ou un seul numéro de registre, qui sert à la fois pour la source et pour la destination, pour l'opérande et le résultat. Il s'agit donc d'un encodage 1-référence, une sorte d'équivalent de l'encodage 2-référence pour les instructions monadiques. Les instructions monadiques de ce type, qui lisent une opérande en RAM et écrivent un résultat en RAM, au même endroit, porte un nom spécifique. Elles sont mine de rien assez courante. Tous les processeurs modernes en supportent quelques unes. Mais nous ne pouvons pas en parler à ce moment là du cours. La raison est que ces instructions sont toutes des instructions utilisées sur les processeurs multicœurs, dans des contextes très spécifiques, que nous n'avons pas encore abordé. Pour rentrer dans le détail, il s'agit d'une sous-classe de ces fameuses instructions atomiques appelées '''instructions ''read-modify-write'''''. Tout cela deviendra plus clair une fois qu'on en sera au chapitre sur les instructions atomiques. Les processeurs peuvent gérer quelques instructions multi-accès dyadiques. De rares instructions lisent deux opérandes en mémoire, mais le résultat est enregistré dans les registres. Par exemple, sur les processeurs x86, l'instruction de comparaison CMPS peut lire deux opérandes en mémoire RAM et les comparer. Le résultat de la comparaison est mémorisé dans le registre d'état, pas ailleurs, qui est adressé implicitement. L'instruction lit ses opérandes dans la mémoire, pas ailleurs. Enfin, il existe de rares processeurs où les opérandes peuvent être lus depuis les registres ou la RAM, idem pour l'écriture du résultat qui peut se faire dans les registres ou en RAM. De tels processeurs ont tous des instructions de taille variable. En effet, les instructions vont d'instructions qui encodent deux/trois registres, à des instructions encodant deux/trois adresses. Autant les premières sont très courtes, autant les autres sont très longues. La seule solution pratique est d'utiliser des instructions de taille variable. ==Le cas des processeurs x86== Après avoir vu la théorie, nous allons étudier l'encodage des instructions des CPU x86, présents dans nos PC. Ils ont un des encodage les plus complexe qui soit. Ils utilisent des instructions de longueur variable, qui font de 1 à 15 octets. Le jeu d'instruction contient aussi un grand nombre d'instructions, qui n'a eu de cesse d'augmenter avec le temps. Mais surtout, les instructions en question sont assez complexes. Il supporte des instructions cst-reg, reg-reg et ''load-op'', mais aussi quelques instructions multi-accès. Nous allons d'abord voir les instructions les plus complexes, histoire de voir un exemple concret d'instructions multi-accès, avant de voir comment sont encodées les instructions en général. ===L'encodage d'une instruction x86=== L'encodage d'une instruction x86 contient au minimum un opcode, qui peut-être complété avec des informations annexes. Premièrement, il peut être précédé d'un préfixe, qui complète l'opcode. Il modifie l'interprétation de l'opcode ou des octets qui suivent. Il existe un paquet de préfixes très différents dont nous ne pouvons pas encore parler ici. Il y a notamment un préfixe spécifique pour les instructions 64 bits. Suivant le préfixe, l'opcode peut passer de 1 à 3 octets. Il peut y avoir au maximum 4 préfixes, chacun codé sur un octet. Ils sont facultatifs. Deuxièmement, l'opcode peut être suivi par un octet qui précise le mode d'adressage. Il est facultatif car certaines instructions utilisent uniquement le mode d'adressage implicite. Il est appelé l'octet ModR/M. Troisièmement, l'octet ModR/M peut être suivi par trois informations, qui sont utilisées pour calculer l'adresse de l'opérande chargée depuis la mémoire. Elles regroupent un octet SIB pour l'adressage base + indice, un ''offset'', pour les adressages base + ''offset'' et/ou une constante immédiate. Les trois peuvent être combinées si le mode d'adressage des deux opérandes le permet, mais cela demande d'avoir une opérande immédiate couplée à une opérande en mode d'adressage base + indice + ''offset''. Ce n'est pas très courant. {|class="wikitable" |- ! Préfixes !! opcode !! Mod R/M !! SIB !! Displacement !! Immediate |- | Préfixes || opcode || Mode d'adressage || registres pour l'adressage base + indice || ''offset'' || Constante immédiate |- | facultatif || obligatoire || facultatif || facultatif || facultatif |- | 0 à 4 octets || 1, 2, 3 octets, souvent un seul || 1 octet || 1 octet || 1, 2 ou 4 octets || 1, 2, 4 octets en 32 bits, peut aller jusqu'à 8 en 64 bits. |} L'octet SIB inclu deux numéros de registres : un pour le registre de base, un autre pour le registre d'indice. Il inclu aussi la taille de l'opérande, qui est utilisée pour multiplier l'indice, avant de l'additionner au registre de base. {|class="wikitable" |+ Octet SIB |- ! Taille de l'opérande !! Base !! Indice |- | Entier || Numéro de registre || Numéro de registre |- | 2 bits || 3 bits || 3 bits |} L'octet Mod R/M est composé de trois champs, appelés MOD, REG et R/M. MOD est codé sur deux bits et précise le mode d'adressage : * 00 pour l'adressage indirect à registre ou l'adressage base + indice. * 01 pour dire qu'un ''offset'' de 1 octet est présent. * 10 pour dire qu'un ''offset'' de 4 octets est présent. * 11 si les deux opérandes sont dans un registre. Encoder des numéros de registres sur 3 bits limite le processeur à 8 registres. Les CPU x86 s'en sont contenté jusqu’à la génération 32 bits. Lors du passage au 64 bits, 8 autres registres ont été ajouté. Pour ajouter des registres, l'octet SIB se voit ajouter deux bits, un par numéro de registre, ce qui fait qu'il est alors codé sur 10 bits. Mais cela n'est le cas que si l'octet d'opcode est précédé par l'octet de préfixe adéquat, le préfixe REX, VEX ou XOP. Pour les opérations dyadiques, REG encode le numéro de registre de la première opérande, R/M encode le numéro de registre pour la seconde opérande. Pour les opérations à une seule opérande, le champ REG est ignoré et le numéro de registre est dans le champ R/M. En clair, l'octet Mod R/M a une interprétation qui dépend de l'opcode. De plus, la destination est encodée via encodage 2-opérande. La destination est soit la première opérande, soit la seconde. Un bit de l'opcode permet de choisir, ce qui donne un peu de flexibilité à l'encodage 2-opérande. {|class="wikitable" |+ Octet SIB |- ! Taille de l'opérande !! Base !! Indice |- | Entier || Numéro de registre || Numéro de registre |- | 2 bits || 3 bits || 3 bits |} ===Les ''string instructions''=== Les instructions dont nous allons parler sont connues sous le nom d'''instructions de chaines de caractère'', bien qu'elles travaillent en réalité plus sur des tableaux ou des zones de mémoire. Les opérations arithmétiques et logiques ne peuvent pas lire deux opérandes en mémoire, la possibilité est donc limitée à quelques instructions d'accès mémoire bien précises. Les instructions de chaine de caractère peuvent se classer en deux types : celles qui ne font qu'un seul accès mémoire, et celles qui en font deux. Les instructions de chaine de caractère multi-accès sont les suivantes : * L'instruction MOVS copie une adresse mémoire dans une autre, c'est la seule instruction de copie mémoire sur ces CPU. * L'instruction de comparaison CMPS peut lire deux opérandes en mémoire RAM et les comparer. Le résultat de la comparaison est mémorisé dans le registre d'état, pas ailleurs. Les autres instructions de chaine de caractère regroupent trois instructions : * LOD charge une donnée dans le registre EAX, l'adresse de cette donnée est dans le registre ESI. * STO copie une donnée en mémoire RAM, la donnée est dans le registre EAX, l'adresse mémoire est dans le registre ESI. * SCA charge une donnée et la compare avec le registre EAX, l'adresse de la donnée est dans le registre ESI. Pour les échanges entre deux adresses mémoire, les processeurs x86 fournissent une instruction dédiée, appelée MOVS. L'adresse de la source est dans le registre ESI, l'adresse de destination est dans le registre EDI. Vous remarquerez que les registres utilisés sont fixés à l'avance, ce qui fait qu'ils sont adressés implicitement. Les deux registres sont incrémentés/décrémentés à chaque exécution de l'instruction, afin de pointer vers le mot mémoire suivant. Fait intéressant, les instructions mémoires peuvent être répétées automatiquement plusieurs fois, en leur ajoutant un préfixe, un octet placé avant l'opcode. Le nombre de répétitions doit être stocké dans le registre ECX, qui est décrémenté à chaque exécution de l'instruction. Une fois que le registre ECX atteint 0, l'instruction n'est plus répétée et on passe à l'instruction suivante. En clair, l'instruction décrémente automatiquement ce registre à chaque exécution et s'arrête quand il atteint zéro. Suivant le préfixe, d'autres conditions peuvent être ajoutées. Elle peut être répétée plusieurs fois avec l'ajout du préfixe REP. L'instruction avec préfixe, REPMOVS, permet donc de copier un bloc de mémoire dans un autre, de copier N adresses consécutives ! Cela permet de parcourir la mémoire dans l'ordre montant (adresses croissantes) ou descendant (adresses décroissantes), suivant que les registres sont incrémentés ou décrémentés. La direction de parcours de la mémoire est spécifiée dans un bit incorporé dans le processeur, qui vaut 0 (décrémentation) ou 1 (incrémentation), et peut être altéré par des instructions dédiées, de manière à être mis à 1 ou 0. D'autres instructions d'accès mémoire peuvent être préfixées avec REP. Par exemple, l'instruction STOS, qui copie le contenu du registre EAX dans une adresse. On peut exécuter cette instruction une fois, ou la répéter plusieurs fois avec le préfixe REP. Là encore, on doit préciser l'adresse à laquelle écrire dans un registre spécifié, puis le nombre de répétitions dans le registre ECX. Là encore, le nombre de répétitions est décrémenté à chaque exécution, alors que l'adresse est incrémentée/décrémentée. L'instruction REPSTOS est très utile pour mettre à zéro une zone de mémoire, ou pour initialiser un tableau avec des données préétablies. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les modes d'adressage | prevText=Les modes d'adressage | next=Les jeux d'instructions | nextText=Les jeux d'instructions }} </noinclude> tl1oz6wrs7cghclgin4jyribwnjq8b6 773858 773856 2026-10-01T13:55:58Z Mewtow 31375 /* L'encodage des opérandes et des références */ 773858 wikitext text/x-wiki Pour rappel, les programmes informatiques sont placés en mémoire, au même titre que les données qu'ils manipulent. En clair, les instructions sont stockées dans la mémoire RAM/ROM sous la forme de suites de bits, tout comme les données. Il est impossible de faire la différence entre donnée et instruction, vu que rien ne ressemble plus à une suite de bits qu'une autre suite de bits. La seule différence est que les instructions sont chargées via le ''program counter'', alors que les données sont lues ou écrites par des instructions d'accès mémoire. En théorie, cette différence fait que le processeur ne peut pas prendre par erreur des instructions pour des données. ==La taille d'une instruction : taille fixe ou variable== Une instruction est codée sur plusieurs bits. Le nombre de bits utilisé pour coder une instruction est appelée la '''taille de l'instruction'''. Sur certains processeurs, la taille d'une instruction est fixe, c’est-à-dire qu'elle est la même pour toutes les instructions. Mais sur d'autres processeurs, les instructions n'ont pas toutes la même taille, ils gèrent des instructions de longueur variable. Un exemple de jeu d’instruction à longueur variable est le x86 des pc actuels, où une instruction peut faire entre 1 et 15 octets. L'encodage des instructions x86 est tellement compliqué qu'il prendrait à lui seul plusieurs chapitres ! Les instructions de longueur variable permettent d'économiser un peu de mémoire : avoir des instructions qui font entre 1 et 3 octets est plus avantageux que de tout mettre sur 3 octets. Mais en contrepartie le chargement de l'instruction suivante par le processeur est rendu plus compliqué. Le processeur doit en effet identifier la longueur de l'instruction courante pour savoir où est la suivante. À l'opposé, des instructions de taille fixe gâchent un peu de mémoire, mais permettent au processeur de calculer plus facilement l’adresse de l'instruction suivante et de la charger plus facilement. Il existe des processeurs qui sont un peu entre les deux, avec des instructions de taille fixe, mais qui ne font pas toutes la même taille. Un exemple est jeu d'instruction RISC-V, où il existe des instructions "normales" de 32 bits et des instructions "compressées" de 16 bits. Le processeur charge un mot de 32 bits, ce qui fait qu'il peut lire entre une et deux instructions à la fois. Au tout début de l'instruction, un bit est mis à 0 ou 1 selon que l'instruction soit longue ou courte. Le reste de l'instruction varie suivant sa longueur. ==L'encodage d'une instruction : généralités== Il est intéressant d'étudier comment les instructions sont encodées, à savoir comment sont organisés ses bits. En effet, une instruction n'est pas qu'une suite de bits sans signification. Elle est en fait découpée en '''champs''', à savoir des suites de bits qui ont une signification. ===L'opcode, aussi appelé "code opération"=== Toute instruction contient un champ opcode, qui indique quelle est l'instruction : est-ce une instruction d'addition, de soustraction, un branchement inconditionnel, un appel de fonction, une lecture en mémoire, etc. Le terme opcode est une abréviation pour "code opération". Typiquement, l'opcode est encodé sur un octet, voire moins. Plus l'opcode est encodé sur un grand nombre de bits, plus le processeur peut supporter un grand nombre d'instruction. Par exemple, un octet d'opcode permet d'encoder 256 instructions différentes, 5 bits d'opcode seulement 32 instructions, etc. Pour la même instruction, l'opcode peut être différent suivant le processeur, ce qui est source d'incompatibilités. Par exemple, les opcodes de l'instruction d'addition ne sont pas les mêmes sur les processeurs x86 (ceux de nos PC) et les anciens macintosh, ou encore les microcontrôleurs. Ce qui fait qu'un opcode de processeur x86 n'aura pas d'équivalent sur un autre processeur, ou correspondra à une instruction totalement différente. En théorie, l'opcode a une taille fixe, ce qui veut dire qu'il est toujours codé sur le même nombre de bits, pour toutes les instructions. Par exemple, sur les processeurs ARM7, l'opcode est encodé sur 8 bits. Il n'y a pas d'instruction qui a un opcode de 5 bits, une autre qui a un opcode de 8 bits, etc. Non, toutes les instructions ont un opcode de 8 bits. Mais il y a des jeux d'instruction qui font exception. Par exemple, sur les processeurs x86, l'opcode peut faire 1, 2 ou 3 octets. Le mécanisme pour gérer des opcodes de taille variable sera expliqué plus tard dans ce chapitre. Il arrive que de rares instructions ne soient composées d'un opcode, sans rien d'autre. Elles ont alors une représentation en binaire qui est unique. : Pour l'anecdote, certains processeurs n'utilisent qu'une seule et unique instruction, et qui peuvent se passer d'opcodes. Mais ce sont des curiosités, pas quelque chose d'utilisé en pratique. ===L'encodage des opérandes et des références=== En plus de l'opcode, une instruction doit préciser où sont ses opérandes. Par opérande, on veut parler des données manipulées par l'instruction. Pour cela, il y a deux cas possibles. * Le premier encode l'opérande dans l'instruction, avec l'adressage immédiat. L'opérande est alors une constante connue à la compilation, elle est intégrée dans l'instruction directement, après l'opcode. * Le second cas indique où se trouve l'opérande, en donnant une référence vers celle-ci. La référence précise dans quel registre ou à quelle adresse se trouve l'opérande. La référence a un champ dédié, rien que pour elle, à la suite de l'opcode. La référence est associée à un mode d'adressage qui précise comment l'interpréter : si c'est un numéro de registre, une adresse mémoire, un pointeur, de quoi calculer l'adresse, etc. Mais laissons de côté le mode d'adressage pour le moment et concentrons-nous sur l'encodage de l'opérande ou de sa référence. Nous allons voir que l'encodage des instructions est compliqué par plusieurs problèmes. Un premier problème est que les instructions n'ont pas le même nombre d'opérandes. Les instructions arithmétiques et logiques sont souvent des '''instructions dyadiques''', à savoir qui prennent deux opérandes et fournissent un résultat. L'addition, la soustraction, les opérations bit à bit ET/OU/XOR, les décalages et rotations, sont dans ce cas. Il existe cependant quelques rares '''instructions monadiques''' qui n'ont qu'une seule opérande et fournissent un résultat, comme les instructions NOT, INC/DEC (incrémentation/décrémentation). Vu qu'elles n'ont pas le même nombre d'opérande, le nombre de bits utilisé pour encoder l'instruction ne sera pas le même. Les opérations d'accès mémoire qui n'ont pas d'opérande à proprement parler, ni même de résultat. Elles doivent cependant savoir quelle adresse elles doivent lire ou écrire. Les instructions LOAD et STORE copient une donnée d'une source vers une destination. Par exemple, l'instruction MOV copie une donnée d'un registre source vers un registre destination, l'instruction LOAD charge une donnée depuis une adresse source vers un registre destination, etc. Et la source comme la destination doivent être encodés dans l'instruction, ce qui est équivalent à encoder deux opérandes. Un second problème, lié au précédent, est que les références n'ont pas toutes la même taille : une adresse utilise plus de bits qu'un numéro de registre, par exemple. En conséquence, une instruction dont les deux références sont des adresses mémoire prendra plus de place qu'un calcul entre deux registres. Les constantes immédiates ont souvent une taille proche de celle des adresses. En effet, les adresses mémoire sont un peu l'équivalent des constantes immédiates, mais pour les instructions d'accès mémoire. La combinaison de ses deux problèmes fait qu'encoder des instructions est compliqué, particulièrement avec des instructions de taille fixe. Les instructions de longueur variables ne sont pas trop gênées par ces deux problèmes. Une instruction de longueur variable prend autant de bits qu'elle a besoin pour encoder ses opérandes et son résultat, ou sa source et sa destination. Une instruction dyadique prendra donc plus de place qu'une instruction monadique. Cependant, elles doivnt faire preuve de mesure et ne pas encoder des instructions super longues. Et c'est très facile d'obtenir deux adresses super longues : une instruction mémoire MOV qui copie une adresse dans une autre, avec deux adresses de 64 bits, peut facilement dépasser les 20 octets ! Les instructions de longueur fixe sont par contre bien plus gênées par les deux problèmes précédents. Elles doivent trouver un moyen pour tout faire rentrer dans 16, 32 ou 64 bits. Une solution systématiquement utilisée est de limiter la taille des adresses et constantes immédiates, qui font généralement 16 bits, rarement plus. Une autre solution, complémentaire de la précédente, est de réduire les possibilités d'adressage. Certaines opérandes sont encodées de manière implicite, on empêche certaines combinaisons d'opérandes, on empêche d’utiliser certains modes d’adressage pour certaines instructions, etc. Leur encodage est donc plus restrictif. Les opérations dyadiques font partie de celles qui subissent des restrictions. Intuitivement, encoder une opération dyadique demande d'encoder trois références : une par opérande, plus une pour le résultat. Le résultat est ce qu'on appelle à tord l'encodage 3-opérandes, en confondant le résultat avec une opérande. Une autre dénomination tout aussi mauvaise est l'encodage encodage 3-adresses car une telle instruction encode 3 adresses, trois noms de registre, etc. Nous parlerons plutôt d''''encodage 3-références''', plus correct, qui indique bien que l’instruction encode trois références. Ce n'est pas parfait car l'une de ces opérandes peut très bien être une constante immédiate, mais passons. Toujours est-il qu'encoder 2 opérandes + 1 résultat prend de la place. Si on prend une instruction encodée sur 32 bits, c'est un petit peu large. Avec un octet par registre et un octet pour l'opcode, on remplit complétement les 32 bits. Et c'est en supposant que le mode d'adressage n'ait pas à être encodé, chose qu'on verra à la section suivante. Par contre, si on souhaite utiliser une constante immédiate, on est fortement limité. Si on prend un processeur avec 16 registres, et un opcode encodé sur un octet, on n'a que 12 bits pour la constante immédiate, ce qui est peu. Heureusement, l'encodage 3-référence peut être modifié pour adresser le résultat de manière implicite, pour économiser un peu de place. Avec cet encodage, le résultat est enregistré dans le registre de la seconde opérande. Pour le dire autrement, l'opérande sera remplacé par le résultat de l'instruction, l'opérande est écrasé. Avec cet encodage, les instructions ne précisent que deux opérandes, pas le résultat, ce qui permet de gagner de la place. On parle alors d''''encodage 2-références''', d'encodage 2-adresses, ou encore d'encodage 2-opérandes. L'encodage 2-références facilite grandement l'usage de l'adressage immédiat. Si on prend une instruction 32 bits, avec un octet d'opcode, cela laisse 24 bits pour encoder un registre et une constante immédiate, ce qui permet d'utiliser des constantes assez larges. Des constantes de 16 bits sont la norme, et on peut atteindre facilement les 20 bits. Mais ce n'est pas leur seul avantage. Un autre avantage est que cela permet de faire tenir des instructions sur seulement 16 bits. Un exemple est celui des processeurs RISC-V, qu'on a mentionné plus haut. Ils ont des instructions 32 bits, et des instructions compressées de 16 bits. Les instructions de 32 bits sont des instructions à trois adresses : elles indiquent deux opérandes et la destination du résultat. Les instructions de 16 bits n'ont que deux opérandes et encodent le résultat de manière implicite. Cela explique qu'elles soient plus courtes : deux opérandes prennent moins de place que trois. Les instructions monadiques utilisent pas défaut l'encodage 2-références, pour préciser l'opérande et le résultat. Mais elles peuvent aussi utiliser l'encodage une-référence, qui est aux instructions monadiques ce qu'est l'encodage 2-référence aux instructions dyadiques. L'idée est que le résultat écrase l'opérande, il la remplace. Le résultat est écrit à la même adresse que l'opérande, ou il est écrit dans le même registre. L'avantage est qu'on a besoin de ne préciser qu'une seule opérande, pas deux. L'instruction est donc plus courte. [[File:CODE2 formatos.jpg|centre|vignette|upright=2|Exemples d'encodage d'instruction sur une taille fixe. </br> 0 - Opération sans opérande. </br> 1 - Instruction mono-opérande en encodage 1-référence. </br> 2 - Instruction de branchement. </br> 3 - Instruction dyadique en encodage 2-opérande. </br> 4 - Instruction dyadique en encodage 3-opérandes.]] ===L'encodage du mode d'adressage=== Il existe deux méthodes principales pour préciser le mode d'adressage utilisé par l'instruction, avec un cas intermédiaire. Dans le premier cas, l'instruction ne gère qu'un mode d'adressage par opérande. Par exemple, prenons le cas d'une instruction COPY qui copie un registre dans un autre. sur les processeurs dit RISC (''Reduced Instruction Set Computer'') , les instructions arithmétiques ne peuvent manipuler que des registres. Un autre cas est celui de l'instruction MOV, qui peut copier un registre ou une constante dans un registre destination. La destination est forcément un registre, pas besoin d'encoder le mode d'adressage pour le registre destination. Par contre, la source doit être encodée explicitement. Dans des cas pareils, le mode d'adressage est déduit automatiquement via l'opcode : on parle de '''mode d'adressage implicite'''. Dans un cas intermédiaire, il se peut que plusieurs instructions existent pour faire la même chose, mais avec des modes d'adressages différents. Le mode d'adressage est alors encodé dans l'opcode, ou en est déduit. Il s'agit d'un cas particulier d'adressage implicite. Un exemple est celui des processeurs ARM, qui ont plusieurs instructions d'addition. Une qui ne manipule que des registres, et adresse trois registres, avec mode adressage implicite. Une autre additionne une constante immédiate avec un registre, ce qui fait que la constante immédiate et les deux registres source/destination sont aussi à mode d'adressage implicite. [[File:Mips32 addi.svg|centre|vignette|upright=2|Exemple d'une instruction avec mode d'adressage implicite, appartenant au jeu d'instruction MIPS.]] Dans le second cas, les instructions gèrent plusieurs modes d'adressage par opérande. Par exemple, une instruction d'addition peut additionner soit deux registres, soit un registre et une adresse, soit un registre et une constante. Dans un cas pareil, l'instruction doit préciser le mode d'adressage utilisé, au moyen de quelques bits intercalés entre l'opcode et les opérandes. On parle de '''mode d'adressage explicite'''. Sur certains processeurs, chaque instruction peut utiliser tous les modes d'adressage supportés par le processeur : on dit que le processeur est orthogonal. [[File:Machine instructions.svg|centre|vignette|upright=2|Exemple d'une instruction avec mode d'adressage explicite.]] Maintenant, tout cela ne vaut que pour encoder le mode d'adressage d'une donnée, pas plus. Mais une instruction gère plusieurs données : ses opérandes, son résultat. Pour les accès mémoire, c'est sa source et de destination. Il faut donc préciser plusieurs modes d'adressage : un par opérande, plus un pour le résultat. Les instructions manipulant plusieurs opérandes peuvent parfois utiliser un mode d'adressage différent pour chaque opérande. Par exemple, une addition manipule deux opérandes, ce qui demande d'utiliser un mode d'adressage par opérande (dans le pire des cas). Il faut donc préciser plusieurs modes d'adressage : un par opérande, plus un pour le résultat. Intuitivement, on se dit qu'il faut utiliser un encodage explicite pour chacune d'entre elles. Idem pour le résultat. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande/résultat est adressée explicitement |- ! Opcode ! Mode d'adressage opérande 1 | Référence vers l'opérande 1 ! Mode d'adressage opérande 2 | Référence vers l'opérande 2 ! Mode d'adressage résultat | Référence vers le résultat |} L'encodage du mode d'adressage prend quelques bits, entre 1 et 4, pas plus. Il est donc intéressant de regrouper les modes d'adressage dans un même octet, dans un même champ. Faire ainsi sépare bien les références/opérandes d'un côté, et les bits de contrôle de l'autre, qui encodent l'instruction proprement dite. Et cela se marie très bien avec les instructions de longueur variable. Elles gardent un cœur de taille fixe auquel on colle des opérandes/références de taille variable. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande/résultat est adressée explicitement |- ! Opcode ! Mode d'adressage opérande 1 ! Mode d'adressage opérande 2 ! Mode d'adressage résultat | Référence vers l'opérande 1 | Référence vers l'opérande 2 | Référence vers le résultat |} Avec l'encodage 2-référence, on n'a pas à encoder explicitement la référence du résultat, ni son mode d'adressage. Cela fait gagner un peu de place. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande est adressée explicitement, le résultat est adressé implicitement |- ! Opcode ! Mode d'adressage opérande 1 ! Mode d'adressage opérande 2/résultat | Référence vers l'opérande 1 | Référence vers l'opérande 2 / le résultat |} Il s'agit là d'un usage de l'encodage implicite du mode d'adressage. Mais il y en a d'autres, comme nous le verrons dans ce qui suit. ===L'encodage de la taille des opérandes=== Enfin, une instruction encode la taille des données qu'elle manipule. C'est surtout utile pour les instructions d'accès mémoire. Il est en théorie possible de se limiter à des instructions mémoire qui lisent/écrivent des données de la même taille que les registres entiers, souvent égal à la taille d'un mot mémoire. Mais dans les faits, seules les architectures anciennes à adressage par mot font ça. De nos jours, les architectures à adressage par byte permettent de préciser qu'on veut lire un entier de 8 bits, 16 bits, 32 bits, 64 bits. Pour cela, la solution la plus simple est d'encoder la taille des données dans l'instruction. On rajoute un champ qui indique quelle est la taille des données à lire/écrire. Le champ n'encode pas un nombre comme 8, 16, 32, 64. À la place, il contient deux bits qui sont à interpréter comme suit : * 00 pour des données de 8 bits ; * 01 pour des données de 16 bits ; * 10 pour des données de 32 bits ; * 11 pour des données de 64 bits. En général, ce champ n'existe que pour les instructions d'accès mémoire, qui sont les seules qui se préoccupent de la taille des opérandes. La taille des données est surtout pertinente quand on lit/écrit en mémoire RAM/ROM. Les opérations arithmétiques sont rarement concernées, car elles manipulent le plus souvent des registres, qui n'ont qu'une seule taille. Il y a des exemples où c'est le cas, comme avec le système d'''alias'' de registres des processeurs x86, mais c'est l'exception plus que la règle. Et de plus, ça ne sert que très rarement de limiter la taille des résultats, surtout qu'une simple opération de masquage peut toujours être employée pour éliminer les bits de poids fort. Quelques jeux d'instructions ajoutent ce système de taille des opérandes pour les opérations arithmétiques simples, comme les additions et soustractions. Il arrive même que certaines opérations gèrent la taille des opérandes, mais pas d'autres, sur certaines jeux d'instruction irréguliers. La raison est une question de compatibilité, par exemple quand un jeu d'instruction de 16 bits a été étendu au 32 bits. Les anciennes instructions gardent leur encodage et travaillent sur 16 bits, d'autres sont ajoutées pour gérer les opérandes 32 bits. Les processeurs x86 utilisent un système totalement différent, avec un pseudo-aliasing des registres, qu'on a vu dans le chapitre sur les registres. ===L'encodage des instructions est avant tout un compromis=== Encoder des instructions est quelque chose d'assez complexe, qui demande de faire un compromis entre des contraintes opposées, contradictoires. Pour savoir ce que doit faire un encodage idéal, faisons une petite liste. Idéalement, un encodage doit utiliser des instructions de taille fixe, afin de simplifier le chargement/décodage des instructions. Avec des instructions de taille variable, le processeur a plus de mal à déterminer où se trouve la prochaine instruction. Il doit déterminer la longueur de l'instruction pour savoir où se trouve la suivante. Alors qu'avec des instructions de taille fixe, le calcul est nettement plus simple, la longueur de l'instruction est connue à l'avance. Les instructions doivent aussi avoir une taille qui est une puissance de deux, afin de simplifier l'alignement des instructions en mémoire. Les instructions peuvent donc faire 8, 16, 32, 64 bits. Et là, voyons chaque taille au cas par cas. * Encoder des instructions sur 8 bits est juste impossible, il n'y a pas assez de bits. Sauf sur des jeux d'instructions spécialisés appelés des machines à pile, qui auront droit à un chapitre dédié. * Encoder des instructions sur 16 bits, c'est possible, mais il faut oublier l'usage de constantes immédiates ou l'adressage absolu, les constantes et adresses ne rentrant pas dans une instruction aussi petite. Concrètement, il faut utiliser des jeux d'instructions spéciaux, comme des architectures à accumulateur, qu'on verra dans quelques chapitres. * Encoder des instructions sur 32 bits est la règle, tous les processeurs font cela de nos jours. Il y a assez de place pour encoder des constantes immédiates et des adresses assez larges, disons 16 ou 24 bits. De même, encoder des instructions trois-opérandes utilisant des registres n'est pas un problème : 32 bits est suffisant pour encoder 4 registres et un opcode. * Encoder des instructions sur 64 bits serait complétement excessif. On aurait assez de place pour mettre plein d'instructions, pleins de registres, des constantes et adresses très larges. Par contre, doubler la taille des instructions signifie doubler la taille du code, les programmes prennent deux fois plus de place qu'avec des instructions 32 bits. Et il faut aussi deux fois plus de débit binaire pour charger le même nombre d'instructions, ce qui implique de doubler la taille du bus qui relie le processeur à la mémoire des instructions. Pour résumer, des instructions de 32 bits sont l'idéal : assez longues pour qu'on puisse encoder des instructions utiles, pas assez longue pour pose problème niveau matériel/mémoire. Il faut un peu pousser pour que ca rentre, si j'ose dire, mais rien d'infaisable. ==Les restrictions sur les modes d'adressage== Intuitivement, on se dit que toute opérande peut utiliser tous les modes d'adressage possibles, il n'y a pas de restrictions particulières. Cependant, ce n'est pas le cas du tout. Permettre à toutes les opérandes d'utiliser tous les modes d'adressage possibles pose quelques problèmes, qu'on ne peut pas résoudre facilement. ===La gestion du mode d'adressage immédiat=== Le premier problème est que de nombreuses combinaisons sont interdites, voire n'ont aucun sens. Par exemple, ça n'aurait aucun sens d'utiliser l'adressage immédiat pour le résultat d'une instruction. De même, ça n'aurait aucun intérêt d'utiliser le mode d'adressage immédiat pour les deux opérandes d'une opération dyadique. Concrètement, le mode d'adressage immédiat ne peut être utilisé que sur un opérande à la fois. De plus, l'adressage immédiat n'a de sens que pour les opérations dyadiques, mais n'a aucun sens avec les opérations monadiques. Une opération sur une constante donnera toujours le même résultat, autant éliminer l'instruction et précalculer ce résultat. En somme, seules les instructions dyadiques peuvent encoder une constante immédiate, pas les autres. Les instructions d'accès mémoire peuvent utiliser une variante de l'adressage immédiat : l'adressage absolu qui encode une adresse directement dans l'instruction. Mais l'encodage est assez similaire. ===Les restrictions de registres source/destination=== Les anciens processeurs avaient des restrictions quant à l'utilisation des registres. Par exemple, certaines instructions ne pouvaient utiliser que certains registres, pas les autres. D'autres avaient un registre de destination prévu à l'avance, d'autres ne pouvaient lire leurs opérandes que dans des registres précis, etc. Voyons cela avec quelques exemples. Les contraintes peuvent porter sur le registre pour le résultat. Un exemple est celui des anciennes architectures MIPS, qui limitaient le registre de résultat pour la multiplication. Les multiplications pouvaient lire leurs opérandes dans tous les registres, mais leur résultat allait dans un registre dédié, séparé des registres généraux, appelé HO/LO. Le registre était en réalité composé de deux registres : le registre LO pour les 32 bits de poids faible du résultat, le registre HO pour les 32 bits de poids fort. Les contraintes peuvent aussi porter sur les opérandes. Elles ne peuvent alors provenir que de certains registres, pas des autres. Et les restrictions peuvent porter sur les deux opérandes, ou alors uniquement sur la première opérande, ou que sur la seconde. Pour donner un exemple, prenons l'exemple des processeurs x86, avec les instructions de décalage. Elle prennent deux opérandes : l'opérande à décaler et le nombre de rangs par lequel il faut décaler. Le nombre de rangs est soit une constante immédiate, soit le registre CL. Les autres registres ne peuvent pas être utilisés pour préciser de combien il faut décaler. La même architecture avait des limitations similaires sur les très anciens processeurs x86, avant l'arrivée du CPU 386. Les opérations de multiplication et d'extension de signe devaient avoir leur première opérande dans le registre AX. De telles contraintes visent à réduire la taille des instructions. Pour reprendre le cas de l'instruction SHL, limiter le nombre de registre opérande à un seul fait qu'on peut l'adresser implicitement. ON n'a pas à encoder le numéro de registre, ce qui économise pas mal de bits. Cela permet à l'instruction de tenir sur un seul octet, deux si l'instruction utilise une constante immédiate. Un autre exemple de contrainte est celui de l'ancien processeur Data General Nova. Il disposait de 4 registres généraux (appelés improprement accumulateurs), mais seul deux d'entre eux servaient pour l'adressage mémoire. Précisément, le processeur gérait trois modes d'adressage, dont l'adressage absolu indicé. Pour rappel, celui-ci additionne une adresse fixe avec un indice variable. L'adresse est intégrée à l'instruction, alors que l'indice est dans un registre. Et sur le Data General Nova, l'indice ne pouvait être que dans les deux derniers registres, pas les deux premiers. ===Les restrictions sur les opérandes mémoire=== Une seconde restriction est liée à la source des opérandes. Un opérande peut provenir de deux sources : des registres, de la mémoire RAM, ou être fournie via adressage immédiat. Le résultat peut lui être enregistré en RAM ou dans les registres. Et cela met en avant un second problème : en faisant cela, une instruction peut effectuer plusieurs accès mémoire. Par exemple, il est possible de lire deux opérandes en mémoire RAM et d'enregistrer le résultat en mémoire RAM. Ou encore de lire deux opérandes en RAM et d'enregistrer le résultat dans les registres. Les processeurs gèrent ce problème de deux manières différentes. La première interdit de faire plusieurs accès mémoire par instruction, ce qui impose des limitations quant à l'utilisation des modes d'adressage. Typiquement, une instruction peut lire un opérande en RAM, mais pas plus. Le jeu d'instruction interdit certaines combinaisons de modes d'adressage avec deux opérandes. La seconde méthode autorise des instructions à faire plusieurs accès mémoire par instruction. Par exemple, une opération dyadique peut lire deux opérandes en RAM, copier une donnée d'une adresse mémoire à une autre, etc. Les modes d'adressages sont moins limités, des combinaisons deviennent possibles. Dans ce qui va suivre, nous allons séparer les instructions en plusieurs types : celles qui ne font pas d'accès mémoire, celles qui en font un, celles qui en font plusieurs. Nous parlerons d''''instruction simple-accès''' pour celles qui font au maximum un accès mémoire. Les instructions qui effectuent plusieurs accès mémoire seront appelées des '''instructions multi-accès'''. Et nous allons les voir séparément. ==L'encodage des instructions simple-accès== Pour commencer, nous allons voir l'encodage des instructions sur les processeurs qui ne gèrent pas plusieurs accès mémoire par instruction. Leur encodage est plus compact que pour les instructions multi-accès. ===Les instructions mémoire simple-accès et leur encodage=== Sur la plupart des processeurs, les instructions ne peuvent faire qu'un seul accès mémoire. Il est interdit de faire plusieurs accès mémoires par instruction. La première conséquence est que les seules instructions d'accès mémoire permises sont : * les instructions LOAD pour charger une donnée de la RAM dans un registre ; * l'instruction STORE pour copier une donnée d'un registre vers la RAM ; * l'instruction MOV en adressage inhérent (registre), qui copie un registre dans un autre ; * l'instruction MOV en adressage immédiat (constante) pour charger une constante immédiate dans un registre destination. D'autres instructions sont possibles, mais elles sont rarement implémentées. Par exemple, on peut citer l'instruction d'échange entre registres XCHG. Une autre possibilité est une variante de l'instruction STORE en mode d'adressage immédiat. Elle copie une constante dans une adresse mémoire. Elle n'est pas présente sur les architectures ARM, POWER PC ou SPARC. Mais laissons-les de côté. Pour rappel, les instructions d'accès mémoire copient une donnée d'une source vers une destination, les deux ayant leur propre mode d'adressage. Il n'est pas systématiquement encodé. Par exemple, l'instruction LOAD charge une donnée dans un registre destination, l'adressage de la destination est donc toujours l'adressage inhérent (nom de registre). Pas besoin d'encoder le mode d'adressage, juste la référence, le nom de registre. Idem pour les autres instructions. {|class="wikitable" |- ! colspan="7" | Instruction LOAD |- | Opcode | Adressage de la source * Adressage absolu (adresse) * Adressage indirect à registre * Adressage base + indice * etc. | Adresse ou référence de la source | | Nom de registre de destination |- ! colspan="7" | Instruction STORE |- | Opcode | | Nom de registre source | Adressage de la destination * Adressage absolu (adresse) * Adressage indirect à registre * Adressage base + indice * etc. | Adresse ou référence de destination |- ! colspan="7" | Instruction MOV |- | rowspan="2" | Opcode | rowspan="2" | * Adressage inhérent (registre) * Adressage immédiat (constante) | Nom de registre source | rowspan="2" | | rowspan="2" | Nom de registre destination |- | Constante immédiate |} Les processeurs x86, et sans doute quelques autres, fusionnent les instructions LOAD, STORE et MOV en une seule instruction appelée MOV. Elle est capable de faire une lecture, une écriture et une copie entre registres. Par contre, elle n'est pas capable de faire une copie d'une adresse mémoire vers une autre. Elles encodent une source et une destination, dont au moins l'une d'entre elle est un registre. Leur encodage place le registre juste après l'opcode, et la seconde opérande après. Un bit de l'instruction dit si la première opérande est la source ou la destination. {|class="wikitable" |+ Encodage d'une instruction MOV généraliste |- | rowspan="3" | Opcode | rowspan="3" | Bit qui indique l'ordre des champs | rowspan="3" | Nom de registre source/destination | rowspan="3" | * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source/destination |- | Adresse mémoire ou registres pour les pointeurs |- | Constante immédiate |} ===Les instructions dyadiques simple-accès et leur encodage=== Les instructions arithmétiques et logique sont elles aussi concernées par la contrainte d'un accès mémoire par instruction. Pour ces opérations, il y a deux possibilités : lire un opérande en RAM, enregistrer le résultat en RAM. Les deux options peuvent s'émuler avec deux instructions consécutives. La première option remplace une instruction LOAD suivie d'une instruction arithmétique/logique, la seconde remplace une instruction arithmétique/logique suivie d'une instruction STORE. Dans les deux cas, on économise un registre : le registre pour charger l'opérande dans le premier cas, le registre pour le résultat dans le second cas. Dans les faits, la seconde option n'est jamais appliquée, le résultat est systématiquement stocké dans un registre. En effet, il est fréquent qu'un opérande soit chargé depuis la mémoire, alors qu'il est rare qu'un résultat soit écrit en mémoire après avoir été calculé. Un résultat calculé a de fortes chances d'être réutilisé par la suite, donc de servir d'opérandes sous peu. Mieux vaut le laisser dans les registres, plutôt que de payer le prix d'un accès mémoire. Lire un opérande en mémoire RAM est par contre beaucoup plus fréquent et donc intéressant. [[File:Isregmem.png|vignette|Instruction dyadique de type ''load-op''.]] Dans ce qui suit, nous parlerons d''''instructions ''load-op''''' pour désigner de telles instructions. Elles lisent un opérande en RAM, mais l'autre opérande est lu depuis les registres et le résultat est enregistré dans les registres. Elles sont opposées aux '''instructions reg-reg''', ce qui est une abréviation pour registre-registre. Ces dernières lisent leurs deux opérandes dans les registres, idem pour l'écriture du résultat. Il faut aussi citer les '''instructions cst-reg''', où un opérande est fourni via adressage immédiat, l'autre opérande est dans les registres. Avec la contrainte d'un seul accès RAM par instruction, ce sont les seuls types d'instruction qui existent. {|class="wikitable" |+ Encodage d'une instruction dyadique reg-reg |- ! rowspan="2" | Opcode | rowspan="2" | * Adressage inhérent (registre) * Adressage immédiat (constante) | Nom de registre pour l'opérande 1 | rowspan="2" | Nom de registre pour l'opérande 2 | rowspan="2" | Nom de registre pour le résultat |- | Constante immédiate |} {|class="wikitable" |+ Encodage d'une instruction ''load-op'' |- ! rowspan="3" | Opcode | rowspan="3" | * Adressage inhérent (registre) * Adressage immédiat (constante) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source | rowspan="3" | | rowspan="3" | Nom de registre destination |- | Constante immédiate |- | Adresse mémoire ou registres pour les pointeurs |} Encoder une instruction ''load-op'' est compliqué. En soit, encoder l'opcode et les numéros de registre ne prend pas beaucoup de place, mais encoder une adresse complète prend beaucoup de bits. Une première solution utilise des instructions de taille variable : les instructions reg-reg sont plus courtes que les instructions ''load-op'', ces dernières prenant plus de place pour coder une adresse. Mais gérer des instructions de taille variable est complexe. Aussi, il existe une solution qui marche avec des instructions de taille fixe, à savoir si toutes les instructions sont codées sur le même nombre d'octets (souvent 2, 4 ou 8 octets selon le processeur) : utiliser l'encodage 2-opérandes. ==Les instructions multi-accès et leur encodage== Passons maintenant aux instructions multi-accès. Nous allons séparer les instructions d'accès mémoire d'un côté, et les instructions dyadiques de l'autre. ===Les instructions mémoire-mémoire=== Les instructions que nous allons voir dans ce qui suit sont des instructions d'accès mémoire, qui sont en plus multi-accès. Elles sont connues sous le nom d''''instruction mémoire-mémoire''', ce qui signifie qu'elle lisent une donnée en mémoire, pour l'enregistrer en mémoire. Les instructions mémoire-mémoire les plus communes sont les '''instructions de copie mémoire''', où une adresse est copiée dans une autre. Les copies en mémoire sont des opérations très fréquentes, il est très fréquent qu'un programme copie un bloc de mémoire dans un autre et beaucoup de programmeurs ont déjà été confronté à un tel cas. Aussi, les processeurs ajoutent des instructions multi-accès pour accélérer ces copies, ce qui fait un bon compromis entre performance et simplicité d'implémentation. Avec elles, la source et la destination sont systématiquement des adresses, mais, il faut préciser leur mode d'adressage : absolu, pointeur, autre. Pour préciser le mode d'adressage, il est possible de préciser un mode d’adressage différent pour la source et de la destination. Par exemple, on peut utiliser le mode d'adressage base + indice pour la source, l'adressage absolu pour la destination. {|class="wikitable" |+ Encodage d'une instruction de copie mémoire |- ! Opcode | Mode d'adressage de la source * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice * etc. | Mode d'adressage de la destination * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice * etc. | Référence(s) vers la source | Référence(s) vers la destination |} Une autre solution utilise le même mode d'adressage pour la source et la destination. On perd en flexibilité, mais ce n'est pas si grave. Les instructions de copie mémoire sont souvent utilisées pour copier des tableaux, qui sont souvent adressés avec l'adressage base + indice ou indirect à registre. Par contre, l'encodage de l'instruction est plus simple, on économise quelques bits. {|class="wikitable" |+ Encodage d'une instruction de copie mémoire |- ! Opcode | Mode d'adressage de la source et de la destination * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice | Référence(s) vers la source | Référence(s) vers la destination |} Les instructions multi-accès précédentes demandent d'encoder deux adresses dans l'instruction, ainsi que le mode d'adressage pour ces deux adresses. Les instructions sont donc assez longues et ne tiennent pas dans une instruction de taille fixe, des instructions de taille variables sont utilisées. Quelques processeurs fusionnent les instructions de copie mémoire avec les instructions LOAD, STORE et MOV. Elles ont alors une '''instruction mémoire généraliste''', souvent appelée MOV. Elle est capable de faire une lecture, une écriture, une copie entre registres et une copie d'une adresse mémoire vers une autre. Source comme destination doivent encoder le mode d'adressage adéquat, elles n'utilisent pas le même mode d'adressage. Typiquement, le premier opérande désigne la source et l'autre la destination. * Si les deux opérandes sont des registres, le premier registre est copié dans le second. * Si le premier opérande est un registre et l'autre une adresse, c'est une écriture. * Si le premier opérande est une adresse et l'autre un registre, c'est une lecture. * Si les deux opérandes sont des adresses, c'est une copie mémoire. {|class="wikitable" |+ Encodage d'une instruction mémoire généraliste |- ! rowspan="3" | Opcode | rowspan="3" | Mode d'adressage de la source * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) * Adressage immédiat (constante) | rowspan="3" | Mode d'adressage de la destination * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source | Nom de registre destination |- | Adresse mémoire ou registres pour les pointeurs | Adresse mémoire ou registres pour les pointeurs |- | Constante immédiate |} ===Les instructions multi-accès monadiques et dyadiques=== Il existe de rares instructions multi-accès monadiques. Un exemple est celui des instructions de décalage sur les CPU x86. L'opérande à décaler est lue soit depuis les registres, soit depuis la mémoire, elle est écrite au même endroit. Elle fait donc deux accès mémoire, si l'opérande est en RAM : un pour lire d'opérande, l'autre pour écrire le résultat. L'instruction fournit une seule adresse ou un seul numéro de registre, qui sert à la fois pour la source et pour la destination, pour l'opérande et le résultat. Il s'agit donc d'un encodage 1-référence, une sorte d'équivalent de l'encodage 2-référence pour les instructions monadiques. Les instructions monadiques de ce type, qui lisent une opérande en RAM et écrivent un résultat en RAM, au même endroit, porte un nom spécifique. Elles sont mine de rien assez courante. Tous les processeurs modernes en supportent quelques unes. Mais nous ne pouvons pas en parler à ce moment là du cours. La raison est que ces instructions sont toutes des instructions utilisées sur les processeurs multicœurs, dans des contextes très spécifiques, que nous n'avons pas encore abordé. Pour rentrer dans le détail, il s'agit d'une sous-classe de ces fameuses instructions atomiques appelées '''instructions ''read-modify-write'''''. Tout cela deviendra plus clair une fois qu'on en sera au chapitre sur les instructions atomiques. Les processeurs peuvent gérer quelques instructions multi-accès dyadiques. De rares instructions lisent deux opérandes en mémoire, mais le résultat est enregistré dans les registres. Par exemple, sur les processeurs x86, l'instruction de comparaison CMPS peut lire deux opérandes en mémoire RAM et les comparer. Le résultat de la comparaison est mémorisé dans le registre d'état, pas ailleurs, qui est adressé implicitement. L'instruction lit ses opérandes dans la mémoire, pas ailleurs. Enfin, il existe de rares processeurs où les opérandes peuvent être lus depuis les registres ou la RAM, idem pour l'écriture du résultat qui peut se faire dans les registres ou en RAM. De tels processeurs ont tous des instructions de taille variable. En effet, les instructions vont d'instructions qui encodent deux/trois registres, à des instructions encodant deux/trois adresses. Autant les premières sont très courtes, autant les autres sont très longues. La seule solution pratique est d'utiliser des instructions de taille variable. ==Le cas des processeurs x86== Après avoir vu la théorie, nous allons étudier l'encodage des instructions des CPU x86, présents dans nos PC. Ils ont un des encodage les plus complexe qui soit. Ils utilisent des instructions de longueur variable, qui font de 1 à 15 octets. Le jeu d'instruction contient aussi un grand nombre d'instructions, qui n'a eu de cesse d'augmenter avec le temps. Mais surtout, les instructions en question sont assez complexes. Il supporte des instructions cst-reg, reg-reg et ''load-op'', mais aussi quelques instructions multi-accès. Nous allons d'abord voir les instructions les plus complexes, histoire de voir un exemple concret d'instructions multi-accès, avant de voir comment sont encodées les instructions en général. ===L'encodage d'une instruction x86=== L'encodage d'une instruction x86 contient au minimum un opcode, qui peut-être complété avec des informations annexes. Premièrement, il peut être précédé d'un préfixe, qui complète l'opcode. Il modifie l'interprétation de l'opcode ou des octets qui suivent. Il existe un paquet de préfixes très différents dont nous ne pouvons pas encore parler ici. Il y a notamment un préfixe spécifique pour les instructions 64 bits. Suivant le préfixe, l'opcode peut passer de 1 à 3 octets. Il peut y avoir au maximum 4 préfixes, chacun codé sur un octet. Ils sont facultatifs. Deuxièmement, l'opcode peut être suivi par un octet qui précise le mode d'adressage. Il est facultatif car certaines instructions utilisent uniquement le mode d'adressage implicite. Il est appelé l'octet ModR/M. Troisièmement, l'octet ModR/M peut être suivi par trois informations, qui sont utilisées pour calculer l'adresse de l'opérande chargée depuis la mémoire. Elles regroupent un octet SIB pour l'adressage base + indice, un ''offset'', pour les adressages base + ''offset'' et/ou une constante immédiate. Les trois peuvent être combinées si le mode d'adressage des deux opérandes le permet, mais cela demande d'avoir une opérande immédiate couplée à une opérande en mode d'adressage base + indice + ''offset''. Ce n'est pas très courant. {|class="wikitable" |- ! Préfixes !! opcode !! Mod R/M !! SIB !! Displacement !! Immediate |- | Préfixes || opcode || Mode d'adressage || registres pour l'adressage base + indice || ''offset'' || Constante immédiate |- | facultatif || obligatoire || facultatif || facultatif || facultatif |- | 0 à 4 octets || 1, 2, 3 octets, souvent un seul || 1 octet || 1 octet || 1, 2 ou 4 octets || 1, 2, 4 octets en 32 bits, peut aller jusqu'à 8 en 64 bits. |} L'octet SIB inclu deux numéros de registres : un pour le registre de base, un autre pour le registre d'indice. Il inclu aussi la taille de l'opérande, qui est utilisée pour multiplier l'indice, avant de l'additionner au registre de base. {|class="wikitable" |+ Octet SIB |- ! Taille de l'opérande !! Base !! Indice |- | Entier || Numéro de registre || Numéro de registre |- | 2 bits || 3 bits || 3 bits |} L'octet Mod R/M est composé de trois champs, appelés MOD, REG et R/M. MOD est codé sur deux bits et précise le mode d'adressage : * 00 pour l'adressage indirect à registre ou l'adressage base + indice. * 01 pour dire qu'un ''offset'' de 1 octet est présent. * 10 pour dire qu'un ''offset'' de 4 octets est présent. * 11 si les deux opérandes sont dans un registre. Encoder des numéros de registres sur 3 bits limite le processeur à 8 registres. Les CPU x86 s'en sont contenté jusqu’à la génération 32 bits. Lors du passage au 64 bits, 8 autres registres ont été ajouté. Pour ajouter des registres, l'octet SIB se voit ajouter deux bits, un par numéro de registre, ce qui fait qu'il est alors codé sur 10 bits. Mais cela n'est le cas que si l'octet d'opcode est précédé par l'octet de préfixe adéquat, le préfixe REX, VEX ou XOP. Pour les opérations dyadiques, REG encode le numéro de registre de la première opérande, R/M encode le numéro de registre pour la seconde opérande. Pour les opérations à une seule opérande, le champ REG est ignoré et le numéro de registre est dans le champ R/M. En clair, l'octet Mod R/M a une interprétation qui dépend de l'opcode. De plus, la destination est encodée via encodage 2-opérande. La destination est soit la première opérande, soit la seconde. Un bit de l'opcode permet de choisir, ce qui donne un peu de flexibilité à l'encodage 2-opérande. {|class="wikitable" |+ Octet SIB |- ! Taille de l'opérande !! Base !! Indice |- | Entier || Numéro de registre || Numéro de registre |- | 2 bits || 3 bits || 3 bits |} ===Les ''string instructions''=== Les instructions dont nous allons parler sont connues sous le nom d'''instructions de chaines de caractère'', bien qu'elles travaillent en réalité plus sur des tableaux ou des zones de mémoire. Les opérations arithmétiques et logiques ne peuvent pas lire deux opérandes en mémoire, la possibilité est donc limitée à quelques instructions d'accès mémoire bien précises. Les instructions de chaine de caractère peuvent se classer en deux types : celles qui ne font qu'un seul accès mémoire, et celles qui en font deux. Les instructions de chaine de caractère multi-accès sont les suivantes : * L'instruction MOVS copie une adresse mémoire dans une autre, c'est la seule instruction de copie mémoire sur ces CPU. * L'instruction de comparaison CMPS peut lire deux opérandes en mémoire RAM et les comparer. Le résultat de la comparaison est mémorisé dans le registre d'état, pas ailleurs. Les autres instructions de chaine de caractère regroupent trois instructions : * LOD charge une donnée dans le registre EAX, l'adresse de cette donnée est dans le registre ESI. * STO copie une donnée en mémoire RAM, la donnée est dans le registre EAX, l'adresse mémoire est dans le registre ESI. * SCA charge une donnée et la compare avec le registre EAX, l'adresse de la donnée est dans le registre ESI. Pour les échanges entre deux adresses mémoire, les processeurs x86 fournissent une instruction dédiée, appelée MOVS. L'adresse de la source est dans le registre ESI, l'adresse de destination est dans le registre EDI. Vous remarquerez que les registres utilisés sont fixés à l'avance, ce qui fait qu'ils sont adressés implicitement. Les deux registres sont incrémentés/décrémentés à chaque exécution de l'instruction, afin de pointer vers le mot mémoire suivant. Fait intéressant, les instructions mémoires peuvent être répétées automatiquement plusieurs fois, en leur ajoutant un préfixe, un octet placé avant l'opcode. Le nombre de répétitions doit être stocké dans le registre ECX, qui est décrémenté à chaque exécution de l'instruction. Une fois que le registre ECX atteint 0, l'instruction n'est plus répétée et on passe à l'instruction suivante. En clair, l'instruction décrémente automatiquement ce registre à chaque exécution et s'arrête quand il atteint zéro. Suivant le préfixe, d'autres conditions peuvent être ajoutées. Elle peut être répétée plusieurs fois avec l'ajout du préfixe REP. L'instruction avec préfixe, REPMOVS, permet donc de copier un bloc de mémoire dans un autre, de copier N adresses consécutives ! Cela permet de parcourir la mémoire dans l'ordre montant (adresses croissantes) ou descendant (adresses décroissantes), suivant que les registres sont incrémentés ou décrémentés. La direction de parcours de la mémoire est spécifiée dans un bit incorporé dans le processeur, qui vaut 0 (décrémentation) ou 1 (incrémentation), et peut être altéré par des instructions dédiées, de manière à être mis à 1 ou 0. D'autres instructions d'accès mémoire peuvent être préfixées avec REP. Par exemple, l'instruction STOS, qui copie le contenu du registre EAX dans une adresse. On peut exécuter cette instruction une fois, ou la répéter plusieurs fois avec le préfixe REP. Là encore, on doit préciser l'adresse à laquelle écrire dans un registre spécifié, puis le nombre de répétitions dans le registre ECX. Là encore, le nombre de répétitions est décrémenté à chaque exécution, alors que l'adresse est incrémentée/décrémentée. L'instruction REPSTOS est très utile pour mettre à zéro une zone de mémoire, ou pour initialiser un tableau avec des données préétablies. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les modes d'adressage | prevText=Les modes d'adressage | next=Les jeux d'instructions | nextText=Les jeux d'instructions }} </noinclude> cudrgh6r8sl9vf60siyxie3lc868w50 773859 773858 2026-10-01T13:57:23Z Mewtow 31375 /* L'encodage des opérandes et des références */ 773859 wikitext text/x-wiki Pour rappel, les programmes informatiques sont placés en mémoire, au même titre que les données qu'ils manipulent. En clair, les instructions sont stockées dans la mémoire RAM/ROM sous la forme de suites de bits, tout comme les données. Il est impossible de faire la différence entre donnée et instruction, vu que rien ne ressemble plus à une suite de bits qu'une autre suite de bits. La seule différence est que les instructions sont chargées via le ''program counter'', alors que les données sont lues ou écrites par des instructions d'accès mémoire. En théorie, cette différence fait que le processeur ne peut pas prendre par erreur des instructions pour des données. ==La taille d'une instruction : taille fixe ou variable== Une instruction est codée sur plusieurs bits. Le nombre de bits utilisé pour coder une instruction est appelée la '''taille de l'instruction'''. Sur certains processeurs, la taille d'une instruction est fixe, c’est-à-dire qu'elle est la même pour toutes les instructions. Mais sur d'autres processeurs, les instructions n'ont pas toutes la même taille, ils gèrent des instructions de longueur variable. Un exemple de jeu d’instruction à longueur variable est le x86 des pc actuels, où une instruction peut faire entre 1 et 15 octets. L'encodage des instructions x86 est tellement compliqué qu'il prendrait à lui seul plusieurs chapitres ! Les instructions de longueur variable permettent d'économiser un peu de mémoire : avoir des instructions qui font entre 1 et 3 octets est plus avantageux que de tout mettre sur 3 octets. Mais en contrepartie le chargement de l'instruction suivante par le processeur est rendu plus compliqué. Le processeur doit en effet identifier la longueur de l'instruction courante pour savoir où est la suivante. À l'opposé, des instructions de taille fixe gâchent un peu de mémoire, mais permettent au processeur de calculer plus facilement l’adresse de l'instruction suivante et de la charger plus facilement. Il existe des processeurs qui sont un peu entre les deux, avec des instructions de taille fixe, mais qui ne font pas toutes la même taille. Un exemple est jeu d'instruction RISC-V, où il existe des instructions "normales" de 32 bits et des instructions "compressées" de 16 bits. Le processeur charge un mot de 32 bits, ce qui fait qu'il peut lire entre une et deux instructions à la fois. Au tout début de l'instruction, un bit est mis à 0 ou 1 selon que l'instruction soit longue ou courte. Le reste de l'instruction varie suivant sa longueur. ==L'encodage d'une instruction : généralités== Il est intéressant d'étudier comment les instructions sont encodées, à savoir comment sont organisés ses bits. En effet, une instruction n'est pas qu'une suite de bits sans signification. Elle est en fait découpée en '''champs''', à savoir des suites de bits qui ont une signification. ===L'opcode, aussi appelé "code opération"=== Toute instruction contient un champ opcode, qui indique quelle est l'instruction : est-ce une instruction d'addition, de soustraction, un branchement inconditionnel, un appel de fonction, une lecture en mémoire, etc. Le terme opcode est une abréviation pour "code opération". Typiquement, l'opcode est encodé sur un octet, voire moins. Plus l'opcode est encodé sur un grand nombre de bits, plus le processeur peut supporter un grand nombre d'instruction. Par exemple, un octet d'opcode permet d'encoder 256 instructions différentes, 5 bits d'opcode seulement 32 instructions, etc. Pour la même instruction, l'opcode peut être différent suivant le processeur, ce qui est source d'incompatibilités. Par exemple, les opcodes de l'instruction d'addition ne sont pas les mêmes sur les processeurs x86 (ceux de nos PC) et les anciens macintosh, ou encore les microcontrôleurs. Ce qui fait qu'un opcode de processeur x86 n'aura pas d'équivalent sur un autre processeur, ou correspondra à une instruction totalement différente. En théorie, l'opcode a une taille fixe, ce qui veut dire qu'il est toujours codé sur le même nombre de bits, pour toutes les instructions. Par exemple, sur les processeurs ARM7, l'opcode est encodé sur 8 bits. Il n'y a pas d'instruction qui a un opcode de 5 bits, une autre qui a un opcode de 8 bits, etc. Non, toutes les instructions ont un opcode de 8 bits. Mais il y a des jeux d'instruction qui font exception. Par exemple, sur les processeurs x86, l'opcode peut faire 1, 2 ou 3 octets. Le mécanisme pour gérer des opcodes de taille variable sera expliqué plus tard dans ce chapitre. Il arrive que de rares instructions ne soient composées d'un opcode, sans rien d'autre. Elles ont alors une représentation en binaire qui est unique. : Pour l'anecdote, certains processeurs n'utilisent qu'une seule et unique instruction, et qui peuvent se passer d'opcodes. Mais ce sont des curiosités, pas quelque chose d'utilisé en pratique. ===L'encodage des opérandes et des références=== En plus de l'opcode, une instruction doit préciser où sont ses opérandes. Par opérande, on veut parler des données manipulées par l'instruction. Pour cela, il y a deux cas possibles. * Le premier encode l'opérande dans l'instruction, avec l'adressage immédiat. L'opérande est alors une constante connue à la compilation, elle est intégrée dans l'instruction directement, après l'opcode. * Le second cas indique où se trouve l'opérande, en donnant une référence vers celle-ci. La référence précise dans quel registre ou à quelle adresse se trouve l'opérande. La référence a un champ dédié, rien que pour elle, à la suite de l'opcode. La référence est associée à un mode d'adressage qui précise comment l'interpréter : si c'est un numéro de registre, une adresse mémoire, un pointeur, de quoi calculer l'adresse, etc. Mais laissons de côté le mode d'adressage pour le moment et concentrons-nous sur l'encodage de l'opérande ou de sa référence. Nous allons voir que l'encodage des instructions est compliqué par plusieurs problèmes. Un premier problème est que les instructions n'ont pas le même nombre d'opérandes. Les instructions arithmétiques et logiques sont souvent des '''instructions dyadiques''', à savoir qui prennent deux opérandes et fournissent un résultat. L'addition, la soustraction, les opérations bit à bit ET/OU/XOR, les décalages et rotations, sont dans ce cas. Il existe cependant quelques rares '''instructions monadiques''' qui n'ont qu'une seule opérande et fournissent un résultat, comme les instructions NOT, INC/DEC (incrémentation/décrémentation). Vu qu'elles n'ont pas le même nombre d'opérande, le nombre de bits utilisé pour encoder l'instruction ne sera pas le même. Les opérations d'accès mémoire qui n'ont pas d'opérande à proprement parler, ni même de résultat. Elles doivent cependant savoir quelle adresse elles doivent lire ou écrire. Les instructions LOAD et STORE copient une donnée d'une source vers une destination. Par exemple, l'instruction MOV copie une donnée d'un registre source vers un registre destination, l'instruction LOAD charge une donnée depuis une adresse source vers un registre destination, etc. Et la source comme la destination doivent être encodés dans l'instruction, ce qui est équivalent à encoder deux opérandes. Un second problème, lié au précédent, est que les références n'ont pas toutes la même taille : une adresse utilise plus de bits qu'un numéro de registre, par exemple. En conséquence, une instruction dont les deux références sont des adresses mémoire prendra plus de place qu'un calcul entre deux registres. Les constantes immédiates ont souvent une taille proche de celle des adresses. En effet, les adresses mémoire sont un peu l'équivalent des constantes immédiates, mais pour les instructions d'accès mémoire. La combinaison de ses deux problèmes fait qu'encoder des instructions est compliqué, particulièrement avec des instructions de taille fixe. Les instructions de longueur variables ne sont pas trop gênées par ces deux problèmes. Une instruction de longueur variable prend autant de bits qu'elle a besoin pour encoder ses opérandes et son résultat, ou sa source et sa destination. Une instruction dyadique prendra donc plus de place qu'une instruction monadique. Cependant, elles doivnt faire preuve de mesure et ne pas encoder des instructions super longues. Et c'est très facile d'obtenir deux adresses super longues : une instruction mémoire MOV qui copie une adresse dans une autre, avec deux adresses de 64 bits, peut facilement dépasser les 20 octets ! Les instructions de longueur fixe sont par contre bien plus gênées par les deux problèmes précédents. Elles doivent trouver un moyen pour tout faire rentrer dans 16, 32 ou 64 bits. Une solution systématiquement utilisée est de limiter la taille des adresses et constantes immédiates, qui font généralement 16 bits, rarement plus. Une autre solution, complémentaire de la précédente, est de réduire les possibilités d'adressage. Certaines opérandes sont encodées de manière implicite, on empêche certaines combinaisons d'opérandes, on empêche d’utiliser certains modes d’adressage pour certaines instructions, etc. Leur encodage est donc plus restrictif. Les opérations dyadiques font partie de celles qui subissent des restrictions. Intuitivement, encoder une opération dyadique demande d'encoder trois références : une par opérande, plus une pour le résultat. Le résultat est ce qu'on appelle à tord l'encodage 3-opérandes, en confondant le résultat avec une opérande. Une autre dénomination tout aussi mauvaise est l'encodage encodage 3-adresses car une telle instruction encode 3 adresses, trois noms de registre, etc. Nous parlerons plutôt d''''encodage 3-références''', plus correct, qui indique bien que l’instruction encode trois références. Ce n'est pas parfait car l'une de ces opérandes peut très bien être une constante immédiate, mais passons. Toujours est-il qu'encoder 2 opérandes + 1 résultat prend de la place. Si on prend une instruction encodée sur 32 bits, c'est un petit peu large. Avec un octet par registre et un octet pour l'opcode, on remplit complétement les 32 bits. Et c'est en supposant que le mode d'adressage n'ait pas à être encodé, chose qu'on verra à la section suivante. Par contre, si on souhaite utiliser une constante immédiate, on est fortement limité. Si on prend un processeur avec 16 registres, et un opcode encodé sur un octet, on n'a que 12 bits pour la constante immédiate, ce qui est peu. Heureusement, l'encodage 3-référence peut être modifié pour adresser le résultat de manière implicite, pour économiser un peu de place. Avec cet encodage, le résultat est enregistré dans le registre de la seconde opérande. Pour le dire autrement, l'opérande sera remplacé par le résultat de l'instruction, l'opérande est écrasé. Avec cet encodage, les instructions ne précisent que deux opérandes, pas le résultat, ce qui permet de gagner de la place. On parle alors d''''encodage 2-références''', d'encodage 2-adresses, ou encore d'encodage 2-opérandes. L'encodage 2-références facilite grandement l'usage de l'adressage immédiat. Si on prend une instruction 32 bits, avec un octet d'opcode, cela laisse 24 bits pour encoder un registre et une constante immédiate, ce qui permet d'utiliser des constantes assez larges. Des constantes de 16 bits sont la norme, et on peut atteindre facilement les 20 bits. Mais ce n'est pas leur seul avantage. Un autre avantage est que cela permet de faire tenir des instructions sur seulement 16 bits. Un exemple est celui des processeurs RISC-V, qu'on a mentionné plus haut. Ils ont des instructions 32 bits, et des instructions compressées de 16 bits. Les instructions de 32 bits sont des instructions à trois adresses : elles indiquent deux opérandes et la destination du résultat. Les instructions de 16 bits n'ont que deux opérandes et encodent le résultat de manière implicite. Cela explique qu'elles soient plus courtes : deux opérandes prennent moins de place que trois. [[File:CODE2 formatos.jpg|centre|vignette|upright=2|Exemples d'encodage d'instruction sur une taille fixe. </br> 0 - Opération sans opérande. </br> 1 - Instruction mono-opérande en encodage 1-référence. </br> 2 - Instruction de branchement. </br> 3 - Instruction dyadique en encodage 2-opérande. </br> 4 - Instruction dyadique en encodage 3-opérandes.]] Les instructions monadiques utilisent par défaut l'encodage 2-références : une pour localiser l'opérande, une autre pour indiquer où enregistrer le résultat. Mais elles peuvent aussi utiliser l'encodage une-référence, qui est aux instructions monadiques ce qu'est l'encodage 2-référence aux instructions dyadiques. L'idée est que le résultat écrase l'opérande, il la remplace. Le résultat est écrit à la même adresse que l'opérande, ou il est écrit dans le même registre. L'avantage est qu'on a besoin de ne préciser qu'une seule opérande, pas deux. L'instruction est donc plus courte. ===L'encodage du mode d'adressage=== Il existe deux méthodes principales pour préciser le mode d'adressage utilisé par l'instruction, avec un cas intermédiaire. Dans le premier cas, l'instruction ne gère qu'un mode d'adressage par opérande. Par exemple, prenons le cas d'une instruction COPY qui copie un registre dans un autre. sur les processeurs dit RISC (''Reduced Instruction Set Computer'') , les instructions arithmétiques ne peuvent manipuler que des registres. Un autre cas est celui de l'instruction MOV, qui peut copier un registre ou une constante dans un registre destination. La destination est forcément un registre, pas besoin d'encoder le mode d'adressage pour le registre destination. Par contre, la source doit être encodée explicitement. Dans des cas pareils, le mode d'adressage est déduit automatiquement via l'opcode : on parle de '''mode d'adressage implicite'''. Dans un cas intermédiaire, il se peut que plusieurs instructions existent pour faire la même chose, mais avec des modes d'adressages différents. Le mode d'adressage est alors encodé dans l'opcode, ou en est déduit. Il s'agit d'un cas particulier d'adressage implicite. Un exemple est celui des processeurs ARM, qui ont plusieurs instructions d'addition. Une qui ne manipule que des registres, et adresse trois registres, avec mode adressage implicite. Une autre additionne une constante immédiate avec un registre, ce qui fait que la constante immédiate et les deux registres source/destination sont aussi à mode d'adressage implicite. [[File:Mips32 addi.svg|centre|vignette|upright=2|Exemple d'une instruction avec mode d'adressage implicite, appartenant au jeu d'instruction MIPS.]] Dans le second cas, les instructions gèrent plusieurs modes d'adressage par opérande. Par exemple, une instruction d'addition peut additionner soit deux registres, soit un registre et une adresse, soit un registre et une constante. Dans un cas pareil, l'instruction doit préciser le mode d'adressage utilisé, au moyen de quelques bits intercalés entre l'opcode et les opérandes. On parle de '''mode d'adressage explicite'''. Sur certains processeurs, chaque instruction peut utiliser tous les modes d'adressage supportés par le processeur : on dit que le processeur est orthogonal. [[File:Machine instructions.svg|centre|vignette|upright=2|Exemple d'une instruction avec mode d'adressage explicite.]] Maintenant, tout cela ne vaut que pour encoder le mode d'adressage d'une donnée, pas plus. Mais une instruction gère plusieurs données : ses opérandes, son résultat. Pour les accès mémoire, c'est sa source et de destination. Il faut donc préciser plusieurs modes d'adressage : un par opérande, plus un pour le résultat. Les instructions manipulant plusieurs opérandes peuvent parfois utiliser un mode d'adressage différent pour chaque opérande. Par exemple, une addition manipule deux opérandes, ce qui demande d'utiliser un mode d'adressage par opérande (dans le pire des cas). Il faut donc préciser plusieurs modes d'adressage : un par opérande, plus un pour le résultat. Intuitivement, on se dit qu'il faut utiliser un encodage explicite pour chacune d'entre elles. Idem pour le résultat. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande/résultat est adressée explicitement |- ! Opcode ! Mode d'adressage opérande 1 | Référence vers l'opérande 1 ! Mode d'adressage opérande 2 | Référence vers l'opérande 2 ! Mode d'adressage résultat | Référence vers le résultat |} L'encodage du mode d'adressage prend quelques bits, entre 1 et 4, pas plus. Il est donc intéressant de regrouper les modes d'adressage dans un même octet, dans un même champ. Faire ainsi sépare bien les références/opérandes d'un côté, et les bits de contrôle de l'autre, qui encodent l'instruction proprement dite. Et cela se marie très bien avec les instructions de longueur variable. Elles gardent un cœur de taille fixe auquel on colle des opérandes/références de taille variable. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande/résultat est adressée explicitement |- ! Opcode ! Mode d'adressage opérande 1 ! Mode d'adressage opérande 2 ! Mode d'adressage résultat | Référence vers l'opérande 1 | Référence vers l'opérande 2 | Référence vers le résultat |} Avec l'encodage 2-référence, on n'a pas à encoder explicitement la référence du résultat, ni son mode d'adressage. Cela fait gagner un peu de place. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande est adressée explicitement, le résultat est adressé implicitement |- ! Opcode ! Mode d'adressage opérande 1 ! Mode d'adressage opérande 2/résultat | Référence vers l'opérande 1 | Référence vers l'opérande 2 / le résultat |} Il s'agit là d'un usage de l'encodage implicite du mode d'adressage. Mais il y en a d'autres, comme nous le verrons dans ce qui suit. ===L'encodage de la taille des opérandes=== Enfin, une instruction encode la taille des données qu'elle manipule. C'est surtout utile pour les instructions d'accès mémoire. Il est en théorie possible de se limiter à des instructions mémoire qui lisent/écrivent des données de la même taille que les registres entiers, souvent égal à la taille d'un mot mémoire. Mais dans les faits, seules les architectures anciennes à adressage par mot font ça. De nos jours, les architectures à adressage par byte permettent de préciser qu'on veut lire un entier de 8 bits, 16 bits, 32 bits, 64 bits. Pour cela, la solution la plus simple est d'encoder la taille des données dans l'instruction. On rajoute un champ qui indique quelle est la taille des données à lire/écrire. Le champ n'encode pas un nombre comme 8, 16, 32, 64. À la place, il contient deux bits qui sont à interpréter comme suit : * 00 pour des données de 8 bits ; * 01 pour des données de 16 bits ; * 10 pour des données de 32 bits ; * 11 pour des données de 64 bits. En général, ce champ n'existe que pour les instructions d'accès mémoire, qui sont les seules qui se préoccupent de la taille des opérandes. La taille des données est surtout pertinente quand on lit/écrit en mémoire RAM/ROM. Les opérations arithmétiques sont rarement concernées, car elles manipulent le plus souvent des registres, qui n'ont qu'une seule taille. Il y a des exemples où c'est le cas, comme avec le système d'''alias'' de registres des processeurs x86, mais c'est l'exception plus que la règle. Et de plus, ça ne sert que très rarement de limiter la taille des résultats, surtout qu'une simple opération de masquage peut toujours être employée pour éliminer les bits de poids fort. Quelques jeux d'instructions ajoutent ce système de taille des opérandes pour les opérations arithmétiques simples, comme les additions et soustractions. Il arrive même que certaines opérations gèrent la taille des opérandes, mais pas d'autres, sur certaines jeux d'instruction irréguliers. La raison est une question de compatibilité, par exemple quand un jeu d'instruction de 16 bits a été étendu au 32 bits. Les anciennes instructions gardent leur encodage et travaillent sur 16 bits, d'autres sont ajoutées pour gérer les opérandes 32 bits. Les processeurs x86 utilisent un système totalement différent, avec un pseudo-aliasing des registres, qu'on a vu dans le chapitre sur les registres. ===L'encodage des instructions est avant tout un compromis=== Encoder des instructions est quelque chose d'assez complexe, qui demande de faire un compromis entre des contraintes opposées, contradictoires. Pour savoir ce que doit faire un encodage idéal, faisons une petite liste. Idéalement, un encodage doit utiliser des instructions de taille fixe, afin de simplifier le chargement/décodage des instructions. Avec des instructions de taille variable, le processeur a plus de mal à déterminer où se trouve la prochaine instruction. Il doit déterminer la longueur de l'instruction pour savoir où se trouve la suivante. Alors qu'avec des instructions de taille fixe, le calcul est nettement plus simple, la longueur de l'instruction est connue à l'avance. Les instructions doivent aussi avoir une taille qui est une puissance de deux, afin de simplifier l'alignement des instructions en mémoire. Les instructions peuvent donc faire 8, 16, 32, 64 bits. Et là, voyons chaque taille au cas par cas. * Encoder des instructions sur 8 bits est juste impossible, il n'y a pas assez de bits. Sauf sur des jeux d'instructions spécialisés appelés des machines à pile, qui auront droit à un chapitre dédié. * Encoder des instructions sur 16 bits, c'est possible, mais il faut oublier l'usage de constantes immédiates ou l'adressage absolu, les constantes et adresses ne rentrant pas dans une instruction aussi petite. Concrètement, il faut utiliser des jeux d'instructions spéciaux, comme des architectures à accumulateur, qu'on verra dans quelques chapitres. * Encoder des instructions sur 32 bits est la règle, tous les processeurs font cela de nos jours. Il y a assez de place pour encoder des constantes immédiates et des adresses assez larges, disons 16 ou 24 bits. De même, encoder des instructions trois-opérandes utilisant des registres n'est pas un problème : 32 bits est suffisant pour encoder 4 registres et un opcode. * Encoder des instructions sur 64 bits serait complétement excessif. On aurait assez de place pour mettre plein d'instructions, pleins de registres, des constantes et adresses très larges. Par contre, doubler la taille des instructions signifie doubler la taille du code, les programmes prennent deux fois plus de place qu'avec des instructions 32 bits. Et il faut aussi deux fois plus de débit binaire pour charger le même nombre d'instructions, ce qui implique de doubler la taille du bus qui relie le processeur à la mémoire des instructions. Pour résumer, des instructions de 32 bits sont l'idéal : assez longues pour qu'on puisse encoder des instructions utiles, pas assez longue pour pose problème niveau matériel/mémoire. Il faut un peu pousser pour que ca rentre, si j'ose dire, mais rien d'infaisable. ==Les restrictions sur les modes d'adressage== Intuitivement, on se dit que toute opérande peut utiliser tous les modes d'adressage possibles, il n'y a pas de restrictions particulières. Cependant, ce n'est pas le cas du tout. Permettre à toutes les opérandes d'utiliser tous les modes d'adressage possibles pose quelques problèmes, qu'on ne peut pas résoudre facilement. ===La gestion du mode d'adressage immédiat=== Le premier problème est que de nombreuses combinaisons sont interdites, voire n'ont aucun sens. Par exemple, ça n'aurait aucun sens d'utiliser l'adressage immédiat pour le résultat d'une instruction. De même, ça n'aurait aucun intérêt d'utiliser le mode d'adressage immédiat pour les deux opérandes d'une opération dyadique. Concrètement, le mode d'adressage immédiat ne peut être utilisé que sur un opérande à la fois. De plus, l'adressage immédiat n'a de sens que pour les opérations dyadiques, mais n'a aucun sens avec les opérations monadiques. Une opération sur une constante donnera toujours le même résultat, autant éliminer l'instruction et précalculer ce résultat. En somme, seules les instructions dyadiques peuvent encoder une constante immédiate, pas les autres. Les instructions d'accès mémoire peuvent utiliser une variante de l'adressage immédiat : l'adressage absolu qui encode une adresse directement dans l'instruction. Mais l'encodage est assez similaire. ===Les restrictions de registres source/destination=== Les anciens processeurs avaient des restrictions quant à l'utilisation des registres. Par exemple, certaines instructions ne pouvaient utiliser que certains registres, pas les autres. D'autres avaient un registre de destination prévu à l'avance, d'autres ne pouvaient lire leurs opérandes que dans des registres précis, etc. Voyons cela avec quelques exemples. Les contraintes peuvent porter sur le registre pour le résultat. Un exemple est celui des anciennes architectures MIPS, qui limitaient le registre de résultat pour la multiplication. Les multiplications pouvaient lire leurs opérandes dans tous les registres, mais leur résultat allait dans un registre dédié, séparé des registres généraux, appelé HO/LO. Le registre était en réalité composé de deux registres : le registre LO pour les 32 bits de poids faible du résultat, le registre HO pour les 32 bits de poids fort. Les contraintes peuvent aussi porter sur les opérandes. Elles ne peuvent alors provenir que de certains registres, pas des autres. Et les restrictions peuvent porter sur les deux opérandes, ou alors uniquement sur la première opérande, ou que sur la seconde. Pour donner un exemple, prenons l'exemple des processeurs x86, avec les instructions de décalage. Elle prennent deux opérandes : l'opérande à décaler et le nombre de rangs par lequel il faut décaler. Le nombre de rangs est soit une constante immédiate, soit le registre CL. Les autres registres ne peuvent pas être utilisés pour préciser de combien il faut décaler. La même architecture avait des limitations similaires sur les très anciens processeurs x86, avant l'arrivée du CPU 386. Les opérations de multiplication et d'extension de signe devaient avoir leur première opérande dans le registre AX. De telles contraintes visent à réduire la taille des instructions. Pour reprendre le cas de l'instruction SHL, limiter le nombre de registre opérande à un seul fait qu'on peut l'adresser implicitement. ON n'a pas à encoder le numéro de registre, ce qui économise pas mal de bits. Cela permet à l'instruction de tenir sur un seul octet, deux si l'instruction utilise une constante immédiate. Un autre exemple de contrainte est celui de l'ancien processeur Data General Nova. Il disposait de 4 registres généraux (appelés improprement accumulateurs), mais seul deux d'entre eux servaient pour l'adressage mémoire. Précisément, le processeur gérait trois modes d'adressage, dont l'adressage absolu indicé. Pour rappel, celui-ci additionne une adresse fixe avec un indice variable. L'adresse est intégrée à l'instruction, alors que l'indice est dans un registre. Et sur le Data General Nova, l'indice ne pouvait être que dans les deux derniers registres, pas les deux premiers. ===Les restrictions sur les opérandes mémoire=== Une seconde restriction est liée à la source des opérandes. Un opérande peut provenir de deux sources : des registres, de la mémoire RAM, ou être fournie via adressage immédiat. Le résultat peut lui être enregistré en RAM ou dans les registres. Et cela met en avant un second problème : en faisant cela, une instruction peut effectuer plusieurs accès mémoire. Par exemple, il est possible de lire deux opérandes en mémoire RAM et d'enregistrer le résultat en mémoire RAM. Ou encore de lire deux opérandes en RAM et d'enregistrer le résultat dans les registres. Les processeurs gèrent ce problème de deux manières différentes. La première interdit de faire plusieurs accès mémoire par instruction, ce qui impose des limitations quant à l'utilisation des modes d'adressage. Typiquement, une instruction peut lire un opérande en RAM, mais pas plus. Le jeu d'instruction interdit certaines combinaisons de modes d'adressage avec deux opérandes. La seconde méthode autorise des instructions à faire plusieurs accès mémoire par instruction. Par exemple, une opération dyadique peut lire deux opérandes en RAM, copier une donnée d'une adresse mémoire à une autre, etc. Les modes d'adressages sont moins limités, des combinaisons deviennent possibles. Dans ce qui va suivre, nous allons séparer les instructions en plusieurs types : celles qui ne font pas d'accès mémoire, celles qui en font un, celles qui en font plusieurs. Nous parlerons d''''instruction simple-accès''' pour celles qui font au maximum un accès mémoire. Les instructions qui effectuent plusieurs accès mémoire seront appelées des '''instructions multi-accès'''. Et nous allons les voir séparément. ==L'encodage des instructions simple-accès== Pour commencer, nous allons voir l'encodage des instructions sur les processeurs qui ne gèrent pas plusieurs accès mémoire par instruction. Leur encodage est plus compact que pour les instructions multi-accès. ===Les instructions mémoire simple-accès et leur encodage=== Sur la plupart des processeurs, les instructions ne peuvent faire qu'un seul accès mémoire. Il est interdit de faire plusieurs accès mémoires par instruction. La première conséquence est que les seules instructions d'accès mémoire permises sont : * les instructions LOAD pour charger une donnée de la RAM dans un registre ; * l'instruction STORE pour copier une donnée d'un registre vers la RAM ; * l'instruction MOV en adressage inhérent (registre), qui copie un registre dans un autre ; * l'instruction MOV en adressage immédiat (constante) pour charger une constante immédiate dans un registre destination. D'autres instructions sont possibles, mais elles sont rarement implémentées. Par exemple, on peut citer l'instruction d'échange entre registres XCHG. Une autre possibilité est une variante de l'instruction STORE en mode d'adressage immédiat. Elle copie une constante dans une adresse mémoire. Elle n'est pas présente sur les architectures ARM, POWER PC ou SPARC. Mais laissons-les de côté. Pour rappel, les instructions d'accès mémoire copient une donnée d'une source vers une destination, les deux ayant leur propre mode d'adressage. Il n'est pas systématiquement encodé. Par exemple, l'instruction LOAD charge une donnée dans un registre destination, l'adressage de la destination est donc toujours l'adressage inhérent (nom de registre). Pas besoin d'encoder le mode d'adressage, juste la référence, le nom de registre. Idem pour les autres instructions. {|class="wikitable" |- ! colspan="7" | Instruction LOAD |- | Opcode | Adressage de la source * Adressage absolu (adresse) * Adressage indirect à registre * Adressage base + indice * etc. | Adresse ou référence de la source | | Nom de registre de destination |- ! colspan="7" | Instruction STORE |- | Opcode | | Nom de registre source | Adressage de la destination * Adressage absolu (adresse) * Adressage indirect à registre * Adressage base + indice * etc. | Adresse ou référence de destination |- ! colspan="7" | Instruction MOV |- | rowspan="2" | Opcode | rowspan="2" | * Adressage inhérent (registre) * Adressage immédiat (constante) | Nom de registre source | rowspan="2" | | rowspan="2" | Nom de registre destination |- | Constante immédiate |} Les processeurs x86, et sans doute quelques autres, fusionnent les instructions LOAD, STORE et MOV en une seule instruction appelée MOV. Elle est capable de faire une lecture, une écriture et une copie entre registres. Par contre, elle n'est pas capable de faire une copie d'une adresse mémoire vers une autre. Elles encodent une source et une destination, dont au moins l'une d'entre elle est un registre. Leur encodage place le registre juste après l'opcode, et la seconde opérande après. Un bit de l'instruction dit si la première opérande est la source ou la destination. {|class="wikitable" |+ Encodage d'une instruction MOV généraliste |- | rowspan="3" | Opcode | rowspan="3" | Bit qui indique l'ordre des champs | rowspan="3" | Nom de registre source/destination | rowspan="3" | * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source/destination |- | Adresse mémoire ou registres pour les pointeurs |- | Constante immédiate |} ===Les instructions dyadiques simple-accès et leur encodage=== Les instructions arithmétiques et logique sont elles aussi concernées par la contrainte d'un accès mémoire par instruction. Pour ces opérations, il y a deux possibilités : lire un opérande en RAM, enregistrer le résultat en RAM. Les deux options peuvent s'émuler avec deux instructions consécutives. La première option remplace une instruction LOAD suivie d'une instruction arithmétique/logique, la seconde remplace une instruction arithmétique/logique suivie d'une instruction STORE. Dans les deux cas, on économise un registre : le registre pour charger l'opérande dans le premier cas, le registre pour le résultat dans le second cas. Dans les faits, la seconde option n'est jamais appliquée, le résultat est systématiquement stocké dans un registre. En effet, il est fréquent qu'un opérande soit chargé depuis la mémoire, alors qu'il est rare qu'un résultat soit écrit en mémoire après avoir été calculé. Un résultat calculé a de fortes chances d'être réutilisé par la suite, donc de servir d'opérandes sous peu. Mieux vaut le laisser dans les registres, plutôt que de payer le prix d'un accès mémoire. Lire un opérande en mémoire RAM est par contre beaucoup plus fréquent et donc intéressant. [[File:Isregmem.png|vignette|Instruction dyadique de type ''load-op''.]] Dans ce qui suit, nous parlerons d''''instructions ''load-op''''' pour désigner de telles instructions. Elles lisent un opérande en RAM, mais l'autre opérande est lu depuis les registres et le résultat est enregistré dans les registres. Elles sont opposées aux '''instructions reg-reg''', ce qui est une abréviation pour registre-registre. Ces dernières lisent leurs deux opérandes dans les registres, idem pour l'écriture du résultat. Il faut aussi citer les '''instructions cst-reg''', où un opérande est fourni via adressage immédiat, l'autre opérande est dans les registres. Avec la contrainte d'un seul accès RAM par instruction, ce sont les seuls types d'instruction qui existent. {|class="wikitable" |+ Encodage d'une instruction dyadique reg-reg |- ! rowspan="2" | Opcode | rowspan="2" | * Adressage inhérent (registre) * Adressage immédiat (constante) | Nom de registre pour l'opérande 1 | rowspan="2" | Nom de registre pour l'opérande 2 | rowspan="2" | Nom de registre pour le résultat |- | Constante immédiate |} {|class="wikitable" |+ Encodage d'une instruction ''load-op'' |- ! rowspan="3" | Opcode | rowspan="3" | * Adressage inhérent (registre) * Adressage immédiat (constante) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source | rowspan="3" | | rowspan="3" | Nom de registre destination |- | Constante immédiate |- | Adresse mémoire ou registres pour les pointeurs |} Encoder une instruction ''load-op'' est compliqué. En soit, encoder l'opcode et les numéros de registre ne prend pas beaucoup de place, mais encoder une adresse complète prend beaucoup de bits. Une première solution utilise des instructions de taille variable : les instructions reg-reg sont plus courtes que les instructions ''load-op'', ces dernières prenant plus de place pour coder une adresse. Mais gérer des instructions de taille variable est complexe. Aussi, il existe une solution qui marche avec des instructions de taille fixe, à savoir si toutes les instructions sont codées sur le même nombre d'octets (souvent 2, 4 ou 8 octets selon le processeur) : utiliser l'encodage 2-opérandes. ==Les instructions multi-accès et leur encodage== Passons maintenant aux instructions multi-accès. Nous allons séparer les instructions d'accès mémoire d'un côté, et les instructions dyadiques de l'autre. ===Les instructions mémoire-mémoire=== Les instructions que nous allons voir dans ce qui suit sont des instructions d'accès mémoire, qui sont en plus multi-accès. Elles sont connues sous le nom d''''instruction mémoire-mémoire''', ce qui signifie qu'elle lisent une donnée en mémoire, pour l'enregistrer en mémoire. Les instructions mémoire-mémoire les plus communes sont les '''instructions de copie mémoire''', où une adresse est copiée dans une autre. Les copies en mémoire sont des opérations très fréquentes, il est très fréquent qu'un programme copie un bloc de mémoire dans un autre et beaucoup de programmeurs ont déjà été confronté à un tel cas. Aussi, les processeurs ajoutent des instructions multi-accès pour accélérer ces copies, ce qui fait un bon compromis entre performance et simplicité d'implémentation. Avec elles, la source et la destination sont systématiquement des adresses, mais, il faut préciser leur mode d'adressage : absolu, pointeur, autre. Pour préciser le mode d'adressage, il est possible de préciser un mode d’adressage différent pour la source et de la destination. Par exemple, on peut utiliser le mode d'adressage base + indice pour la source, l'adressage absolu pour la destination. {|class="wikitable" |+ Encodage d'une instruction de copie mémoire |- ! Opcode | Mode d'adressage de la source * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice * etc. | Mode d'adressage de la destination * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice * etc. | Référence(s) vers la source | Référence(s) vers la destination |} Une autre solution utilise le même mode d'adressage pour la source et la destination. On perd en flexibilité, mais ce n'est pas si grave. Les instructions de copie mémoire sont souvent utilisées pour copier des tableaux, qui sont souvent adressés avec l'adressage base + indice ou indirect à registre. Par contre, l'encodage de l'instruction est plus simple, on économise quelques bits. {|class="wikitable" |+ Encodage d'une instruction de copie mémoire |- ! Opcode | Mode d'adressage de la source et de la destination * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice | Référence(s) vers la source | Référence(s) vers la destination |} Les instructions multi-accès précédentes demandent d'encoder deux adresses dans l'instruction, ainsi que le mode d'adressage pour ces deux adresses. Les instructions sont donc assez longues et ne tiennent pas dans une instruction de taille fixe, des instructions de taille variables sont utilisées. Quelques processeurs fusionnent les instructions de copie mémoire avec les instructions LOAD, STORE et MOV. Elles ont alors une '''instruction mémoire généraliste''', souvent appelée MOV. Elle est capable de faire une lecture, une écriture, une copie entre registres et une copie d'une adresse mémoire vers une autre. Source comme destination doivent encoder le mode d'adressage adéquat, elles n'utilisent pas le même mode d'adressage. Typiquement, le premier opérande désigne la source et l'autre la destination. * Si les deux opérandes sont des registres, le premier registre est copié dans le second. * Si le premier opérande est un registre et l'autre une adresse, c'est une écriture. * Si le premier opérande est une adresse et l'autre un registre, c'est une lecture. * Si les deux opérandes sont des adresses, c'est une copie mémoire. {|class="wikitable" |+ Encodage d'une instruction mémoire généraliste |- ! rowspan="3" | Opcode | rowspan="3" | Mode d'adressage de la source * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) * Adressage immédiat (constante) | rowspan="3" | Mode d'adressage de la destination * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source | Nom de registre destination |- | Adresse mémoire ou registres pour les pointeurs | Adresse mémoire ou registres pour les pointeurs |- | Constante immédiate |} ===Les instructions multi-accès monadiques et dyadiques=== Il existe de rares instructions multi-accès monadiques. Un exemple est celui des instructions de décalage sur les CPU x86. L'opérande à décaler est lue soit depuis les registres, soit depuis la mémoire, elle est écrite au même endroit. Elle fait donc deux accès mémoire, si l'opérande est en RAM : un pour lire d'opérande, l'autre pour écrire le résultat. L'instruction fournit une seule adresse ou un seul numéro de registre, qui sert à la fois pour la source et pour la destination, pour l'opérande et le résultat. Il s'agit donc d'un encodage 1-référence, une sorte d'équivalent de l'encodage 2-référence pour les instructions monadiques. Les instructions monadiques de ce type, qui lisent une opérande en RAM et écrivent un résultat en RAM, au même endroit, porte un nom spécifique. Elles sont mine de rien assez courante. Tous les processeurs modernes en supportent quelques unes. Mais nous ne pouvons pas en parler à ce moment là du cours. La raison est que ces instructions sont toutes des instructions utilisées sur les processeurs multicœurs, dans des contextes très spécifiques, que nous n'avons pas encore abordé. Pour rentrer dans le détail, il s'agit d'une sous-classe de ces fameuses instructions atomiques appelées '''instructions ''read-modify-write'''''. Tout cela deviendra plus clair une fois qu'on en sera au chapitre sur les instructions atomiques. Les processeurs peuvent gérer quelques instructions multi-accès dyadiques. De rares instructions lisent deux opérandes en mémoire, mais le résultat est enregistré dans les registres. Par exemple, sur les processeurs x86, l'instruction de comparaison CMPS peut lire deux opérandes en mémoire RAM et les comparer. Le résultat de la comparaison est mémorisé dans le registre d'état, pas ailleurs, qui est adressé implicitement. L'instruction lit ses opérandes dans la mémoire, pas ailleurs. Enfin, il existe de rares processeurs où les opérandes peuvent être lus depuis les registres ou la RAM, idem pour l'écriture du résultat qui peut se faire dans les registres ou en RAM. De tels processeurs ont tous des instructions de taille variable. En effet, les instructions vont d'instructions qui encodent deux/trois registres, à des instructions encodant deux/trois adresses. Autant les premières sont très courtes, autant les autres sont très longues. La seule solution pratique est d'utiliser des instructions de taille variable. ==Le cas des processeurs x86== Après avoir vu la théorie, nous allons étudier l'encodage des instructions des CPU x86, présents dans nos PC. Ils ont un des encodage les plus complexe qui soit. Ils utilisent des instructions de longueur variable, qui font de 1 à 15 octets. Le jeu d'instruction contient aussi un grand nombre d'instructions, qui n'a eu de cesse d'augmenter avec le temps. Mais surtout, les instructions en question sont assez complexes. Il supporte des instructions cst-reg, reg-reg et ''load-op'', mais aussi quelques instructions multi-accès. Nous allons d'abord voir les instructions les plus complexes, histoire de voir un exemple concret d'instructions multi-accès, avant de voir comment sont encodées les instructions en général. ===L'encodage d'une instruction x86=== L'encodage d'une instruction x86 contient au minimum un opcode, qui peut-être complété avec des informations annexes. Premièrement, il peut être précédé d'un préfixe, qui complète l'opcode. Il modifie l'interprétation de l'opcode ou des octets qui suivent. Il existe un paquet de préfixes très différents dont nous ne pouvons pas encore parler ici. Il y a notamment un préfixe spécifique pour les instructions 64 bits. Suivant le préfixe, l'opcode peut passer de 1 à 3 octets. Il peut y avoir au maximum 4 préfixes, chacun codé sur un octet. Ils sont facultatifs. Deuxièmement, l'opcode peut être suivi par un octet qui précise le mode d'adressage. Il est facultatif car certaines instructions utilisent uniquement le mode d'adressage implicite. Il est appelé l'octet ModR/M. Troisièmement, l'octet ModR/M peut être suivi par trois informations, qui sont utilisées pour calculer l'adresse de l'opérande chargée depuis la mémoire. Elles regroupent un octet SIB pour l'adressage base + indice, un ''offset'', pour les adressages base + ''offset'' et/ou une constante immédiate. Les trois peuvent être combinées si le mode d'adressage des deux opérandes le permet, mais cela demande d'avoir une opérande immédiate couplée à une opérande en mode d'adressage base + indice + ''offset''. Ce n'est pas très courant. {|class="wikitable" |- ! Préfixes !! opcode !! Mod R/M !! SIB !! Displacement !! Immediate |- | Préfixes || opcode || Mode d'adressage || registres pour l'adressage base + indice || ''offset'' || Constante immédiate |- | facultatif || obligatoire || facultatif || facultatif || facultatif |- | 0 à 4 octets || 1, 2, 3 octets, souvent un seul || 1 octet || 1 octet || 1, 2 ou 4 octets || 1, 2, 4 octets en 32 bits, peut aller jusqu'à 8 en 64 bits. |} L'octet SIB inclu deux numéros de registres : un pour le registre de base, un autre pour le registre d'indice. Il inclu aussi la taille de l'opérande, qui est utilisée pour multiplier l'indice, avant de l'additionner au registre de base. {|class="wikitable" |+ Octet SIB |- ! Taille de l'opérande !! Base !! Indice |- | Entier || Numéro de registre || Numéro de registre |- | 2 bits || 3 bits || 3 bits |} L'octet Mod R/M est composé de trois champs, appelés MOD, REG et R/M. MOD est codé sur deux bits et précise le mode d'adressage : * 00 pour l'adressage indirect à registre ou l'adressage base + indice. * 01 pour dire qu'un ''offset'' de 1 octet est présent. * 10 pour dire qu'un ''offset'' de 4 octets est présent. * 11 si les deux opérandes sont dans un registre. Encoder des numéros de registres sur 3 bits limite le processeur à 8 registres. Les CPU x86 s'en sont contenté jusqu’à la génération 32 bits. Lors du passage au 64 bits, 8 autres registres ont été ajouté. Pour ajouter des registres, l'octet SIB se voit ajouter deux bits, un par numéro de registre, ce qui fait qu'il est alors codé sur 10 bits. Mais cela n'est le cas que si l'octet d'opcode est précédé par l'octet de préfixe adéquat, le préfixe REX, VEX ou XOP. Pour les opérations dyadiques, REG encode le numéro de registre de la première opérande, R/M encode le numéro de registre pour la seconde opérande. Pour les opérations à une seule opérande, le champ REG est ignoré et le numéro de registre est dans le champ R/M. En clair, l'octet Mod R/M a une interprétation qui dépend de l'opcode. De plus, la destination est encodée via encodage 2-opérande. La destination est soit la première opérande, soit la seconde. Un bit de l'opcode permet de choisir, ce qui donne un peu de flexibilité à l'encodage 2-opérande. {|class="wikitable" |+ Octet SIB |- ! Taille de l'opérande !! Base !! Indice |- | Entier || Numéro de registre || Numéro de registre |- | 2 bits || 3 bits || 3 bits |} ===Les ''string instructions''=== Les instructions dont nous allons parler sont connues sous le nom d'''instructions de chaines de caractère'', bien qu'elles travaillent en réalité plus sur des tableaux ou des zones de mémoire. Les opérations arithmétiques et logiques ne peuvent pas lire deux opérandes en mémoire, la possibilité est donc limitée à quelques instructions d'accès mémoire bien précises. Les instructions de chaine de caractère peuvent se classer en deux types : celles qui ne font qu'un seul accès mémoire, et celles qui en font deux. Les instructions de chaine de caractère multi-accès sont les suivantes : * L'instruction MOVS copie une adresse mémoire dans une autre, c'est la seule instruction de copie mémoire sur ces CPU. * L'instruction de comparaison CMPS peut lire deux opérandes en mémoire RAM et les comparer. Le résultat de la comparaison est mémorisé dans le registre d'état, pas ailleurs. Les autres instructions de chaine de caractère regroupent trois instructions : * LOD charge une donnée dans le registre EAX, l'adresse de cette donnée est dans le registre ESI. * STO copie une donnée en mémoire RAM, la donnée est dans le registre EAX, l'adresse mémoire est dans le registre ESI. * SCA charge une donnée et la compare avec le registre EAX, l'adresse de la donnée est dans le registre ESI. Pour les échanges entre deux adresses mémoire, les processeurs x86 fournissent une instruction dédiée, appelée MOVS. L'adresse de la source est dans le registre ESI, l'adresse de destination est dans le registre EDI. Vous remarquerez que les registres utilisés sont fixés à l'avance, ce qui fait qu'ils sont adressés implicitement. Les deux registres sont incrémentés/décrémentés à chaque exécution de l'instruction, afin de pointer vers le mot mémoire suivant. Fait intéressant, les instructions mémoires peuvent être répétées automatiquement plusieurs fois, en leur ajoutant un préfixe, un octet placé avant l'opcode. Le nombre de répétitions doit être stocké dans le registre ECX, qui est décrémenté à chaque exécution de l'instruction. Une fois que le registre ECX atteint 0, l'instruction n'est plus répétée et on passe à l'instruction suivante. En clair, l'instruction décrémente automatiquement ce registre à chaque exécution et s'arrête quand il atteint zéro. Suivant le préfixe, d'autres conditions peuvent être ajoutées. Elle peut être répétée plusieurs fois avec l'ajout du préfixe REP. L'instruction avec préfixe, REPMOVS, permet donc de copier un bloc de mémoire dans un autre, de copier N adresses consécutives ! Cela permet de parcourir la mémoire dans l'ordre montant (adresses croissantes) ou descendant (adresses décroissantes), suivant que les registres sont incrémentés ou décrémentés. La direction de parcours de la mémoire est spécifiée dans un bit incorporé dans le processeur, qui vaut 0 (décrémentation) ou 1 (incrémentation), et peut être altéré par des instructions dédiées, de manière à être mis à 1 ou 0. D'autres instructions d'accès mémoire peuvent être préfixées avec REP. Par exemple, l'instruction STOS, qui copie le contenu du registre EAX dans une adresse. On peut exécuter cette instruction une fois, ou la répéter plusieurs fois avec le préfixe REP. Là encore, on doit préciser l'adresse à laquelle écrire dans un registre spécifié, puis le nombre de répétitions dans le registre ECX. Là encore, le nombre de répétitions est décrémenté à chaque exécution, alors que l'adresse est incrémentée/décrémentée. L'instruction REPSTOS est très utile pour mettre à zéro une zone de mémoire, ou pour initialiser un tableau avec des données préétablies. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les modes d'adressage | prevText=Les modes d'adressage | next=Les jeux d'instructions | nextText=Les jeux d'instructions }} </noinclude> lnq050clfhw4foibc567eoxh826fi5b 773860 773859 2026-10-01T14:04:42Z Mewtow 31375 /* La taille d'une instruction : taille fixe ou variable */ 773860 wikitext text/x-wiki Pour rappel, les programmes informatiques sont placés en mémoire, au même titre que les données qu'ils manipulent. En clair, les instructions sont stockées dans la mémoire RAM/ROM sous la forme de suites de bits, tout comme les données. Il est impossible de faire la différence entre donnée et instruction, vu que rien ne ressemble plus à une suite de bits qu'une autre suite de bits. La seule différence est que les instructions sont chargées via le ''program counter'', alors que les données sont lues ou écrites par des instructions d'accès mémoire. En théorie, cette différence fait que le processeur ne peut pas prendre par erreur des instructions pour des données. ==La taille d'une instruction : taille fixe ou variable== Une instruction est codée sur plusieurs bits. Le nombre de bits utilisé pour coder une instruction est appelée la '''taille de l'instruction'''. Sur certains processeurs, la taille d'une instruction est fixe, c’est-à-dire qu'elle est la même pour toutes les instructions. Mais sur d'autres processeurs, les instructions n'ont pas toutes la même taille, ils gèrent des instructions de longueur variable. Un exemple de jeu d’instruction à longueur variable est le x86 des pc actuels, où une instruction peut faire entre 1 et 15 octets. L'encodage des instructions x86 est tellement compliqué qu'il prendrait à lui seul plusieurs chapitres ! Les instructions de longueur variable permettent d'économiser un peu de mémoire : avoir des instructions qui font entre 1 et 3 octets est plus avantageux que de tout mettre sur 3 octets. Mais en contrepartie le chargement de l'instruction suivante par le processeur est rendu plus compliqué. Le processeur doit en effet identifier la longueur de l'instruction courante pour savoir où est la suivante. À l'opposé, des instructions de taille fixe gâchent un peu de mémoire, mais permettent au processeur de calculer plus facilement l’adresse de l'instruction suivante et de la charger plus facilement. Il existe des processeurs qui sont un peu entre les deux, avec des instructions de taille fixe, mais qui ne font pas toutes la même taille. Un exemple est jeu d'instruction RISC-V, où il existe des instructions "normales" de 32 bits et des instructions "compressées" de 16 bits. Le processeur charge un mot de 32 bits, ce qui fait qu'il peut lire entre une et deux instructions à la fois. Au tout début de l'instruction, un bit est mis à 0 ou 1 selon que l'instruction soit longue ou courte. Le reste de l'instruction varie suivant sa longueur. Le jeu d'instruction z-architecture, utilisé sur certains processeurs d'IBM, a des instructions de taille variable. Ses instructions font soit 16 bits, soit 32 bits, soit 64 bits. Remarquez que les tailles des instructions sont des puissances de deux, ce qui simplifie grandement l'alignement mémoire. ==L'encodage d'une instruction : généralités== Il est intéressant d'étudier comment les instructions sont encodées, à savoir comment sont organisés ses bits. En effet, une instruction n'est pas qu'une suite de bits sans signification. Elle est en fait découpée en '''champs''', à savoir des suites de bits qui ont une signification. ===L'opcode, aussi appelé "code opération"=== Toute instruction contient un champ opcode, qui indique quelle est l'instruction : est-ce une instruction d'addition, de soustraction, un branchement inconditionnel, un appel de fonction, une lecture en mémoire, etc. Le terme opcode est une abréviation pour "code opération". Typiquement, l'opcode est encodé sur un octet, voire moins. Plus l'opcode est encodé sur un grand nombre de bits, plus le processeur peut supporter un grand nombre d'instruction. Par exemple, un octet d'opcode permet d'encoder 256 instructions différentes, 5 bits d'opcode seulement 32 instructions, etc. Pour la même instruction, l'opcode peut être différent suivant le processeur, ce qui est source d'incompatibilités. Par exemple, les opcodes de l'instruction d'addition ne sont pas les mêmes sur les processeurs x86 (ceux de nos PC) et les anciens macintosh, ou encore les microcontrôleurs. Ce qui fait qu'un opcode de processeur x86 n'aura pas d'équivalent sur un autre processeur, ou correspondra à une instruction totalement différente. En théorie, l'opcode a une taille fixe, ce qui veut dire qu'il est toujours codé sur le même nombre de bits, pour toutes les instructions. Par exemple, sur les processeurs ARM7, l'opcode est encodé sur 8 bits. Il n'y a pas d'instruction qui a un opcode de 5 bits, une autre qui a un opcode de 8 bits, etc. Non, toutes les instructions ont un opcode de 8 bits. Mais il y a des jeux d'instruction qui font exception. Par exemple, sur les processeurs x86, l'opcode peut faire 1, 2 ou 3 octets. Le mécanisme pour gérer des opcodes de taille variable sera expliqué plus tard dans ce chapitre. Il arrive que de rares instructions ne soient composées d'un opcode, sans rien d'autre. Elles ont alors une représentation en binaire qui est unique. : Pour l'anecdote, certains processeurs n'utilisent qu'une seule et unique instruction, et qui peuvent se passer d'opcodes. Mais ce sont des curiosités, pas quelque chose d'utilisé en pratique. ===L'encodage des opérandes et des références=== En plus de l'opcode, une instruction doit préciser où sont ses opérandes. Par opérande, on veut parler des données manipulées par l'instruction. Pour cela, il y a deux cas possibles. * Le premier encode l'opérande dans l'instruction, avec l'adressage immédiat. L'opérande est alors une constante connue à la compilation, elle est intégrée dans l'instruction directement, après l'opcode. * Le second cas indique où se trouve l'opérande, en donnant une référence vers celle-ci. La référence précise dans quel registre ou à quelle adresse se trouve l'opérande. La référence a un champ dédié, rien que pour elle, à la suite de l'opcode. La référence est associée à un mode d'adressage qui précise comment l'interpréter : si c'est un numéro de registre, une adresse mémoire, un pointeur, de quoi calculer l'adresse, etc. Mais laissons de côté le mode d'adressage pour le moment et concentrons-nous sur l'encodage de l'opérande ou de sa référence. Nous allons voir que l'encodage des instructions est compliqué par plusieurs problèmes. Un premier problème est que les instructions n'ont pas le même nombre d'opérandes. Les instructions arithmétiques et logiques sont souvent des '''instructions dyadiques''', à savoir qui prennent deux opérandes et fournissent un résultat. L'addition, la soustraction, les opérations bit à bit ET/OU/XOR, les décalages et rotations, sont dans ce cas. Il existe cependant quelques rares '''instructions monadiques''' qui n'ont qu'une seule opérande et fournissent un résultat, comme les instructions NOT, INC/DEC (incrémentation/décrémentation). Vu qu'elles n'ont pas le même nombre d'opérande, le nombre de bits utilisé pour encoder l'instruction ne sera pas le même. Les opérations d'accès mémoire qui n'ont pas d'opérande à proprement parler, ni même de résultat. Elles doivent cependant savoir quelle adresse elles doivent lire ou écrire. Les instructions LOAD et STORE copient une donnée d'une source vers une destination. Par exemple, l'instruction MOV copie une donnée d'un registre source vers un registre destination, l'instruction LOAD charge une donnée depuis une adresse source vers un registre destination, etc. Et la source comme la destination doivent être encodés dans l'instruction, ce qui est équivalent à encoder deux opérandes. Un second problème, lié au précédent, est que les références n'ont pas toutes la même taille : une adresse utilise plus de bits qu'un numéro de registre, par exemple. En conséquence, une instruction dont les deux références sont des adresses mémoire prendra plus de place qu'un calcul entre deux registres. Les constantes immédiates ont souvent une taille proche de celle des adresses. En effet, les adresses mémoire sont un peu l'équivalent des constantes immédiates, mais pour les instructions d'accès mémoire. La combinaison de ses deux problèmes fait qu'encoder des instructions est compliqué, particulièrement avec des instructions de taille fixe. Les instructions de longueur variables ne sont pas trop gênées par ces deux problèmes. Une instruction de longueur variable prend autant de bits qu'elle a besoin pour encoder ses opérandes et son résultat, ou sa source et sa destination. Une instruction dyadique prendra donc plus de place qu'une instruction monadique. Cependant, elles doivnt faire preuve de mesure et ne pas encoder des instructions super longues. Et c'est très facile d'obtenir deux adresses super longues : une instruction mémoire MOV qui copie une adresse dans une autre, avec deux adresses de 64 bits, peut facilement dépasser les 20 octets ! Les instructions de longueur fixe sont par contre bien plus gênées par les deux problèmes précédents. Elles doivent trouver un moyen pour tout faire rentrer dans 16, 32 ou 64 bits. Une solution systématiquement utilisée est de limiter la taille des adresses et constantes immédiates, qui font généralement 16 bits, rarement plus. Une autre solution, complémentaire de la précédente, est de réduire les possibilités d'adressage. Certaines opérandes sont encodées de manière implicite, on empêche certaines combinaisons d'opérandes, on empêche d’utiliser certains modes d’adressage pour certaines instructions, etc. Leur encodage est donc plus restrictif. Les opérations dyadiques font partie de celles qui subissent des restrictions. Intuitivement, encoder une opération dyadique demande d'encoder trois références : une par opérande, plus une pour le résultat. Le résultat est ce qu'on appelle à tord l'encodage 3-opérandes, en confondant le résultat avec une opérande. Une autre dénomination tout aussi mauvaise est l'encodage encodage 3-adresses car une telle instruction encode 3 adresses, trois noms de registre, etc. Nous parlerons plutôt d''''encodage 3-références''', plus correct, qui indique bien que l’instruction encode trois références. Ce n'est pas parfait car l'une de ces opérandes peut très bien être une constante immédiate, mais passons. Toujours est-il qu'encoder 2 opérandes + 1 résultat prend de la place. Si on prend une instruction encodée sur 32 bits, c'est un petit peu large. Avec un octet par registre et un octet pour l'opcode, on remplit complétement les 32 bits. Et c'est en supposant que le mode d'adressage n'ait pas à être encodé, chose qu'on verra à la section suivante. Par contre, si on souhaite utiliser une constante immédiate, on est fortement limité. Si on prend un processeur avec 16 registres, et un opcode encodé sur un octet, on n'a que 12 bits pour la constante immédiate, ce qui est peu. Heureusement, l'encodage 3-référence peut être modifié pour adresser le résultat de manière implicite, pour économiser un peu de place. Avec cet encodage, le résultat est enregistré dans le registre de la seconde opérande. Pour le dire autrement, l'opérande sera remplacé par le résultat de l'instruction, l'opérande est écrasé. Avec cet encodage, les instructions ne précisent que deux opérandes, pas le résultat, ce qui permet de gagner de la place. On parle alors d''''encodage 2-références''', d'encodage 2-adresses, ou encore d'encodage 2-opérandes. L'encodage 2-références facilite grandement l'usage de l'adressage immédiat. Si on prend une instruction 32 bits, avec un octet d'opcode, cela laisse 24 bits pour encoder un registre et une constante immédiate, ce qui permet d'utiliser des constantes assez larges. Des constantes de 16 bits sont la norme, et on peut atteindre facilement les 20 bits. Mais ce n'est pas leur seul avantage. Un autre avantage est que cela permet de faire tenir des instructions sur seulement 16 bits. Un exemple est celui des processeurs RISC-V, qu'on a mentionné plus haut. Ils ont des instructions 32 bits, et des instructions compressées de 16 bits. Les instructions de 32 bits sont des instructions à trois adresses : elles indiquent deux opérandes et la destination du résultat. Les instructions de 16 bits n'ont que deux opérandes et encodent le résultat de manière implicite. Cela explique qu'elles soient plus courtes : deux opérandes prennent moins de place que trois. [[File:CODE2 formatos.jpg|centre|vignette|upright=2|Exemples d'encodage d'instruction sur une taille fixe. </br> 0 - Opération sans opérande. </br> 1 - Instruction mono-opérande en encodage 1-référence. </br> 2 - Instruction de branchement. </br> 3 - Instruction dyadique en encodage 2-opérande. </br> 4 - Instruction dyadique en encodage 3-opérandes.]] Les instructions monadiques utilisent par défaut l'encodage 2-références : une pour localiser l'opérande, une autre pour indiquer où enregistrer le résultat. Mais elles peuvent aussi utiliser l'encodage une-référence, qui est aux instructions monadiques ce qu'est l'encodage 2-référence aux instructions dyadiques. L'idée est que le résultat écrase l'opérande, il la remplace. Le résultat est écrit à la même adresse que l'opérande, ou il est écrit dans le même registre. L'avantage est qu'on a besoin de ne préciser qu'une seule opérande, pas deux. L'instruction est donc plus courte. ===L'encodage du mode d'adressage=== Il existe deux méthodes principales pour préciser le mode d'adressage utilisé par l'instruction, avec un cas intermédiaire. Dans le premier cas, l'instruction ne gère qu'un mode d'adressage par opérande. Par exemple, prenons le cas d'une instruction COPY qui copie un registre dans un autre. sur les processeurs dit RISC (''Reduced Instruction Set Computer'') , les instructions arithmétiques ne peuvent manipuler que des registres. Un autre cas est celui de l'instruction MOV, qui peut copier un registre ou une constante dans un registre destination. La destination est forcément un registre, pas besoin d'encoder le mode d'adressage pour le registre destination. Par contre, la source doit être encodée explicitement. Dans des cas pareils, le mode d'adressage est déduit automatiquement via l'opcode : on parle de '''mode d'adressage implicite'''. Dans un cas intermédiaire, il se peut que plusieurs instructions existent pour faire la même chose, mais avec des modes d'adressages différents. Le mode d'adressage est alors encodé dans l'opcode, ou en est déduit. Il s'agit d'un cas particulier d'adressage implicite. Un exemple est celui des processeurs ARM, qui ont plusieurs instructions d'addition. Une qui ne manipule que des registres, et adresse trois registres, avec mode adressage implicite. Une autre additionne une constante immédiate avec un registre, ce qui fait que la constante immédiate et les deux registres source/destination sont aussi à mode d'adressage implicite. [[File:Mips32 addi.svg|centre|vignette|upright=2|Exemple d'une instruction avec mode d'adressage implicite, appartenant au jeu d'instruction MIPS.]] Dans le second cas, les instructions gèrent plusieurs modes d'adressage par opérande. Par exemple, une instruction d'addition peut additionner soit deux registres, soit un registre et une adresse, soit un registre et une constante. Dans un cas pareil, l'instruction doit préciser le mode d'adressage utilisé, au moyen de quelques bits intercalés entre l'opcode et les opérandes. On parle de '''mode d'adressage explicite'''. Sur certains processeurs, chaque instruction peut utiliser tous les modes d'adressage supportés par le processeur : on dit que le processeur est orthogonal. [[File:Machine instructions.svg|centre|vignette|upright=2|Exemple d'une instruction avec mode d'adressage explicite.]] Maintenant, tout cela ne vaut que pour encoder le mode d'adressage d'une donnée, pas plus. Mais une instruction gère plusieurs données : ses opérandes, son résultat. Pour les accès mémoire, c'est sa source et de destination. Il faut donc préciser plusieurs modes d'adressage : un par opérande, plus un pour le résultat. Les instructions manipulant plusieurs opérandes peuvent parfois utiliser un mode d'adressage différent pour chaque opérande. Par exemple, une addition manipule deux opérandes, ce qui demande d'utiliser un mode d'adressage par opérande (dans le pire des cas). Il faut donc préciser plusieurs modes d'adressage : un par opérande, plus un pour le résultat. Intuitivement, on se dit qu'il faut utiliser un encodage explicite pour chacune d'entre elles. Idem pour le résultat. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande/résultat est adressée explicitement |- ! Opcode ! Mode d'adressage opérande 1 | Référence vers l'opérande 1 ! Mode d'adressage opérande 2 | Référence vers l'opérande 2 ! Mode d'adressage résultat | Référence vers le résultat |} L'encodage du mode d'adressage prend quelques bits, entre 1 et 4, pas plus. Il est donc intéressant de regrouper les modes d'adressage dans un même octet, dans un même champ. Faire ainsi sépare bien les références/opérandes d'un côté, et les bits de contrôle de l'autre, qui encodent l'instruction proprement dite. Et cela se marie très bien avec les instructions de longueur variable. Elles gardent un cœur de taille fixe auquel on colle des opérandes/références de taille variable. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande/résultat est adressée explicitement |- ! Opcode ! Mode d'adressage opérande 1 ! Mode d'adressage opérande 2 ! Mode d'adressage résultat | Référence vers l'opérande 1 | Référence vers l'opérande 2 | Référence vers le résultat |} Avec l'encodage 2-référence, on n'a pas à encoder explicitement la référence du résultat, ni son mode d'adressage. Cela fait gagner un peu de place. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande est adressée explicitement, le résultat est adressé implicitement |- ! Opcode ! Mode d'adressage opérande 1 ! Mode d'adressage opérande 2/résultat | Référence vers l'opérande 1 | Référence vers l'opérande 2 / le résultat |} Il s'agit là d'un usage de l'encodage implicite du mode d'adressage. Mais il y en a d'autres, comme nous le verrons dans ce qui suit. ===L'encodage de la taille des opérandes=== Enfin, une instruction encode la taille des données qu'elle manipule. C'est surtout utile pour les instructions d'accès mémoire. Il est en théorie possible de se limiter à des instructions mémoire qui lisent/écrivent des données de la même taille que les registres entiers, souvent égal à la taille d'un mot mémoire. Mais dans les faits, seules les architectures anciennes à adressage par mot font ça. De nos jours, les architectures à adressage par byte permettent de préciser qu'on veut lire un entier de 8 bits, 16 bits, 32 bits, 64 bits. Pour cela, la solution la plus simple est d'encoder la taille des données dans l'instruction. On rajoute un champ qui indique quelle est la taille des données à lire/écrire. Le champ n'encode pas un nombre comme 8, 16, 32, 64. À la place, il contient deux bits qui sont à interpréter comme suit : * 00 pour des données de 8 bits ; * 01 pour des données de 16 bits ; * 10 pour des données de 32 bits ; * 11 pour des données de 64 bits. En général, ce champ n'existe que pour les instructions d'accès mémoire, qui sont les seules qui se préoccupent de la taille des opérandes. La taille des données est surtout pertinente quand on lit/écrit en mémoire RAM/ROM. Les opérations arithmétiques sont rarement concernées, car elles manipulent le plus souvent des registres, qui n'ont qu'une seule taille. Il y a des exemples où c'est le cas, comme avec le système d'''alias'' de registres des processeurs x86, mais c'est l'exception plus que la règle. Et de plus, ça ne sert que très rarement de limiter la taille des résultats, surtout qu'une simple opération de masquage peut toujours être employée pour éliminer les bits de poids fort. Quelques jeux d'instructions ajoutent ce système de taille des opérandes pour les opérations arithmétiques simples, comme les additions et soustractions. Il arrive même que certaines opérations gèrent la taille des opérandes, mais pas d'autres, sur certaines jeux d'instruction irréguliers. La raison est une question de compatibilité, par exemple quand un jeu d'instruction de 16 bits a été étendu au 32 bits. Les anciennes instructions gardent leur encodage et travaillent sur 16 bits, d'autres sont ajoutées pour gérer les opérandes 32 bits. Les processeurs x86 utilisent un système totalement différent, avec un pseudo-aliasing des registres, qu'on a vu dans le chapitre sur les registres. ===L'encodage des instructions est avant tout un compromis=== Encoder des instructions est quelque chose d'assez complexe, qui demande de faire un compromis entre des contraintes opposées, contradictoires. Pour savoir ce que doit faire un encodage idéal, faisons une petite liste. Idéalement, un encodage doit utiliser des instructions de taille fixe, afin de simplifier le chargement/décodage des instructions. Avec des instructions de taille variable, le processeur a plus de mal à déterminer où se trouve la prochaine instruction. Il doit déterminer la longueur de l'instruction pour savoir où se trouve la suivante. Alors qu'avec des instructions de taille fixe, le calcul est nettement plus simple, la longueur de l'instruction est connue à l'avance. Les instructions doivent aussi avoir une taille qui est une puissance de deux, afin de simplifier l'alignement des instructions en mémoire. Les instructions peuvent donc faire 8, 16, 32, 64 bits. Et là, voyons chaque taille au cas par cas. * Encoder des instructions sur 8 bits est juste impossible, il n'y a pas assez de bits. Sauf sur des jeux d'instructions spécialisés appelés des machines à pile, qui auront droit à un chapitre dédié. * Encoder des instructions sur 16 bits, c'est possible, mais il faut oublier l'usage de constantes immédiates ou l'adressage absolu, les constantes et adresses ne rentrant pas dans une instruction aussi petite. Concrètement, il faut utiliser des jeux d'instructions spéciaux, comme des architectures à accumulateur, qu'on verra dans quelques chapitres. * Encoder des instructions sur 32 bits est la règle, tous les processeurs font cela de nos jours. Il y a assez de place pour encoder des constantes immédiates et des adresses assez larges, disons 16 ou 24 bits. De même, encoder des instructions trois-opérandes utilisant des registres n'est pas un problème : 32 bits est suffisant pour encoder 4 registres et un opcode. * Encoder des instructions sur 64 bits serait complétement excessif. On aurait assez de place pour mettre plein d'instructions, pleins de registres, des constantes et adresses très larges. Par contre, doubler la taille des instructions signifie doubler la taille du code, les programmes prennent deux fois plus de place qu'avec des instructions 32 bits. Et il faut aussi deux fois plus de débit binaire pour charger le même nombre d'instructions, ce qui implique de doubler la taille du bus qui relie le processeur à la mémoire des instructions. Pour résumer, des instructions de 32 bits sont l'idéal : assez longues pour qu'on puisse encoder des instructions utiles, pas assez longue pour pose problème niveau matériel/mémoire. Il faut un peu pousser pour que ca rentre, si j'ose dire, mais rien d'infaisable. ==Les restrictions sur les modes d'adressage== Intuitivement, on se dit que toute opérande peut utiliser tous les modes d'adressage possibles, il n'y a pas de restrictions particulières. Cependant, ce n'est pas le cas du tout. Permettre à toutes les opérandes d'utiliser tous les modes d'adressage possibles pose quelques problèmes, qu'on ne peut pas résoudre facilement. ===La gestion du mode d'adressage immédiat=== Le premier problème est que de nombreuses combinaisons sont interdites, voire n'ont aucun sens. Par exemple, ça n'aurait aucun sens d'utiliser l'adressage immédiat pour le résultat d'une instruction. De même, ça n'aurait aucun intérêt d'utiliser le mode d'adressage immédiat pour les deux opérandes d'une opération dyadique. Concrètement, le mode d'adressage immédiat ne peut être utilisé que sur un opérande à la fois. De plus, l'adressage immédiat n'a de sens que pour les opérations dyadiques, mais n'a aucun sens avec les opérations monadiques. Une opération sur une constante donnera toujours le même résultat, autant éliminer l'instruction et précalculer ce résultat. En somme, seules les instructions dyadiques peuvent encoder une constante immédiate, pas les autres. Les instructions d'accès mémoire peuvent utiliser une variante de l'adressage immédiat : l'adressage absolu qui encode une adresse directement dans l'instruction. Mais l'encodage est assez similaire. ===Les restrictions de registres source/destination=== Les anciens processeurs avaient des restrictions quant à l'utilisation des registres. Par exemple, certaines instructions ne pouvaient utiliser que certains registres, pas les autres. D'autres avaient un registre de destination prévu à l'avance, d'autres ne pouvaient lire leurs opérandes que dans des registres précis, etc. Voyons cela avec quelques exemples. Les contraintes peuvent porter sur le registre pour le résultat. Un exemple est celui des anciennes architectures MIPS, qui limitaient le registre de résultat pour la multiplication. Les multiplications pouvaient lire leurs opérandes dans tous les registres, mais leur résultat allait dans un registre dédié, séparé des registres généraux, appelé HO/LO. Le registre était en réalité composé de deux registres : le registre LO pour les 32 bits de poids faible du résultat, le registre HO pour les 32 bits de poids fort. Les contraintes peuvent aussi porter sur les opérandes. Elles ne peuvent alors provenir que de certains registres, pas des autres. Et les restrictions peuvent porter sur les deux opérandes, ou alors uniquement sur la première opérande, ou que sur la seconde. Pour donner un exemple, prenons l'exemple des processeurs x86, avec les instructions de décalage. Elle prennent deux opérandes : l'opérande à décaler et le nombre de rangs par lequel il faut décaler. Le nombre de rangs est soit une constante immédiate, soit le registre CL. Les autres registres ne peuvent pas être utilisés pour préciser de combien il faut décaler. La même architecture avait des limitations similaires sur les très anciens processeurs x86, avant l'arrivée du CPU 386. Les opérations de multiplication et d'extension de signe devaient avoir leur première opérande dans le registre AX. De telles contraintes visent à réduire la taille des instructions. Pour reprendre le cas de l'instruction SHL, limiter le nombre de registre opérande à un seul fait qu'on peut l'adresser implicitement. ON n'a pas à encoder le numéro de registre, ce qui économise pas mal de bits. Cela permet à l'instruction de tenir sur un seul octet, deux si l'instruction utilise une constante immédiate. Un autre exemple de contrainte est celui de l'ancien processeur Data General Nova. Il disposait de 4 registres généraux (appelés improprement accumulateurs), mais seul deux d'entre eux servaient pour l'adressage mémoire. Précisément, le processeur gérait trois modes d'adressage, dont l'adressage absolu indicé. Pour rappel, celui-ci additionne une adresse fixe avec un indice variable. L'adresse est intégrée à l'instruction, alors que l'indice est dans un registre. Et sur le Data General Nova, l'indice ne pouvait être que dans les deux derniers registres, pas les deux premiers. ===Les restrictions sur les opérandes mémoire=== Une seconde restriction est liée à la source des opérandes. Un opérande peut provenir de deux sources : des registres, de la mémoire RAM, ou être fournie via adressage immédiat. Le résultat peut lui être enregistré en RAM ou dans les registres. Et cela met en avant un second problème : en faisant cela, une instruction peut effectuer plusieurs accès mémoire. Par exemple, il est possible de lire deux opérandes en mémoire RAM et d'enregistrer le résultat en mémoire RAM. Ou encore de lire deux opérandes en RAM et d'enregistrer le résultat dans les registres. Les processeurs gèrent ce problème de deux manières différentes. La première interdit de faire plusieurs accès mémoire par instruction, ce qui impose des limitations quant à l'utilisation des modes d'adressage. Typiquement, une instruction peut lire un opérande en RAM, mais pas plus. Le jeu d'instruction interdit certaines combinaisons de modes d'adressage avec deux opérandes. La seconde méthode autorise des instructions à faire plusieurs accès mémoire par instruction. Par exemple, une opération dyadique peut lire deux opérandes en RAM, copier une donnée d'une adresse mémoire à une autre, etc. Les modes d'adressages sont moins limités, des combinaisons deviennent possibles. Dans ce qui va suivre, nous allons séparer les instructions en plusieurs types : celles qui ne font pas d'accès mémoire, celles qui en font un, celles qui en font plusieurs. Nous parlerons d''''instruction simple-accès''' pour celles qui font au maximum un accès mémoire. Les instructions qui effectuent plusieurs accès mémoire seront appelées des '''instructions multi-accès'''. Et nous allons les voir séparément. ==L'encodage des instructions simple-accès== Pour commencer, nous allons voir l'encodage des instructions sur les processeurs qui ne gèrent pas plusieurs accès mémoire par instruction. Leur encodage est plus compact que pour les instructions multi-accès. ===Les instructions mémoire simple-accès et leur encodage=== Sur la plupart des processeurs, les instructions ne peuvent faire qu'un seul accès mémoire. Il est interdit de faire plusieurs accès mémoires par instruction. La première conséquence est que les seules instructions d'accès mémoire permises sont : * les instructions LOAD pour charger une donnée de la RAM dans un registre ; * l'instruction STORE pour copier une donnée d'un registre vers la RAM ; * l'instruction MOV en adressage inhérent (registre), qui copie un registre dans un autre ; * l'instruction MOV en adressage immédiat (constante) pour charger une constante immédiate dans un registre destination. D'autres instructions sont possibles, mais elles sont rarement implémentées. Par exemple, on peut citer l'instruction d'échange entre registres XCHG. Une autre possibilité est une variante de l'instruction STORE en mode d'adressage immédiat. Elle copie une constante dans une adresse mémoire. Elle n'est pas présente sur les architectures ARM, POWER PC ou SPARC. Mais laissons-les de côté. Pour rappel, les instructions d'accès mémoire copient une donnée d'une source vers une destination, les deux ayant leur propre mode d'adressage. Il n'est pas systématiquement encodé. Par exemple, l'instruction LOAD charge une donnée dans un registre destination, l'adressage de la destination est donc toujours l'adressage inhérent (nom de registre). Pas besoin d'encoder le mode d'adressage, juste la référence, le nom de registre. Idem pour les autres instructions. {|class="wikitable" |- ! colspan="7" | Instruction LOAD |- | Opcode | Adressage de la source * Adressage absolu (adresse) * Adressage indirect à registre * Adressage base + indice * etc. | Adresse ou référence de la source | | Nom de registre de destination |- ! colspan="7" | Instruction STORE |- | Opcode | | Nom de registre source | Adressage de la destination * Adressage absolu (adresse) * Adressage indirect à registre * Adressage base + indice * etc. | Adresse ou référence de destination |- ! colspan="7" | Instruction MOV |- | rowspan="2" | Opcode | rowspan="2" | * Adressage inhérent (registre) * Adressage immédiat (constante) | Nom de registre source | rowspan="2" | | rowspan="2" | Nom de registre destination |- | Constante immédiate |} Les processeurs x86, et sans doute quelques autres, fusionnent les instructions LOAD, STORE et MOV en une seule instruction appelée MOV. Elle est capable de faire une lecture, une écriture et une copie entre registres. Par contre, elle n'est pas capable de faire une copie d'une adresse mémoire vers une autre. Elles encodent une source et une destination, dont au moins l'une d'entre elle est un registre. Leur encodage place le registre juste après l'opcode, et la seconde opérande après. Un bit de l'instruction dit si la première opérande est la source ou la destination. {|class="wikitable" |+ Encodage d'une instruction MOV généraliste |- | rowspan="3" | Opcode | rowspan="3" | Bit qui indique l'ordre des champs | rowspan="3" | Nom de registre source/destination | rowspan="3" | * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source/destination |- | Adresse mémoire ou registres pour les pointeurs |- | Constante immédiate |} ===Les instructions dyadiques simple-accès et leur encodage=== Les instructions arithmétiques et logique sont elles aussi concernées par la contrainte d'un accès mémoire par instruction. Pour ces opérations, il y a deux possibilités : lire un opérande en RAM, enregistrer le résultat en RAM. Les deux options peuvent s'émuler avec deux instructions consécutives. La première option remplace une instruction LOAD suivie d'une instruction arithmétique/logique, la seconde remplace une instruction arithmétique/logique suivie d'une instruction STORE. Dans les deux cas, on économise un registre : le registre pour charger l'opérande dans le premier cas, le registre pour le résultat dans le second cas. Dans les faits, la seconde option n'est jamais appliquée, le résultat est systématiquement stocké dans un registre. En effet, il est fréquent qu'un opérande soit chargé depuis la mémoire, alors qu'il est rare qu'un résultat soit écrit en mémoire après avoir été calculé. Un résultat calculé a de fortes chances d'être réutilisé par la suite, donc de servir d'opérandes sous peu. Mieux vaut le laisser dans les registres, plutôt que de payer le prix d'un accès mémoire. Lire un opérande en mémoire RAM est par contre beaucoup plus fréquent et donc intéressant. [[File:Isregmem.png|vignette|Instruction dyadique de type ''load-op''.]] Dans ce qui suit, nous parlerons d''''instructions ''load-op''''' pour désigner de telles instructions. Elles lisent un opérande en RAM, mais l'autre opérande est lu depuis les registres et le résultat est enregistré dans les registres. Elles sont opposées aux '''instructions reg-reg''', ce qui est une abréviation pour registre-registre. Ces dernières lisent leurs deux opérandes dans les registres, idem pour l'écriture du résultat. Il faut aussi citer les '''instructions cst-reg''', où un opérande est fourni via adressage immédiat, l'autre opérande est dans les registres. Avec la contrainte d'un seul accès RAM par instruction, ce sont les seuls types d'instruction qui existent. {|class="wikitable" |+ Encodage d'une instruction dyadique reg-reg |- ! rowspan="2" | Opcode | rowspan="2" | * Adressage inhérent (registre) * Adressage immédiat (constante) | Nom de registre pour l'opérande 1 | rowspan="2" | Nom de registre pour l'opérande 2 | rowspan="2" | Nom de registre pour le résultat |- | Constante immédiate |} {|class="wikitable" |+ Encodage d'une instruction ''load-op'' |- ! rowspan="3" | Opcode | rowspan="3" | * Adressage inhérent (registre) * Adressage immédiat (constante) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source | rowspan="3" | | rowspan="3" | Nom de registre destination |- | Constante immédiate |- | Adresse mémoire ou registres pour les pointeurs |} Encoder une instruction ''load-op'' est compliqué. En soit, encoder l'opcode et les numéros de registre ne prend pas beaucoup de place, mais encoder une adresse complète prend beaucoup de bits. Une première solution utilise des instructions de taille variable : les instructions reg-reg sont plus courtes que les instructions ''load-op'', ces dernières prenant plus de place pour coder une adresse. Mais gérer des instructions de taille variable est complexe. Aussi, il existe une solution qui marche avec des instructions de taille fixe, à savoir si toutes les instructions sont codées sur le même nombre d'octets (souvent 2, 4 ou 8 octets selon le processeur) : utiliser l'encodage 2-opérandes. ==Les instructions multi-accès et leur encodage== Passons maintenant aux instructions multi-accès. Nous allons séparer les instructions d'accès mémoire d'un côté, et les instructions dyadiques de l'autre. ===Les instructions mémoire-mémoire=== Les instructions que nous allons voir dans ce qui suit sont des instructions d'accès mémoire, qui sont en plus multi-accès. Elles sont connues sous le nom d''''instruction mémoire-mémoire''', ce qui signifie qu'elle lisent une donnée en mémoire, pour l'enregistrer en mémoire. Les instructions mémoire-mémoire les plus communes sont les '''instructions de copie mémoire''', où une adresse est copiée dans une autre. Les copies en mémoire sont des opérations très fréquentes, il est très fréquent qu'un programme copie un bloc de mémoire dans un autre et beaucoup de programmeurs ont déjà été confronté à un tel cas. Aussi, les processeurs ajoutent des instructions multi-accès pour accélérer ces copies, ce qui fait un bon compromis entre performance et simplicité d'implémentation. Avec elles, la source et la destination sont systématiquement des adresses, mais, il faut préciser leur mode d'adressage : absolu, pointeur, autre. Pour préciser le mode d'adressage, il est possible de préciser un mode d’adressage différent pour la source et de la destination. Par exemple, on peut utiliser le mode d'adressage base + indice pour la source, l'adressage absolu pour la destination. {|class="wikitable" |+ Encodage d'une instruction de copie mémoire |- ! Opcode | Mode d'adressage de la source * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice * etc. | Mode d'adressage de la destination * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice * etc. | Référence(s) vers la source | Référence(s) vers la destination |} Une autre solution utilise le même mode d'adressage pour la source et la destination. On perd en flexibilité, mais ce n'est pas si grave. Les instructions de copie mémoire sont souvent utilisées pour copier des tableaux, qui sont souvent adressés avec l'adressage base + indice ou indirect à registre. Par contre, l'encodage de l'instruction est plus simple, on économise quelques bits. {|class="wikitable" |+ Encodage d'une instruction de copie mémoire |- ! Opcode | Mode d'adressage de la source et de la destination * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice | Référence(s) vers la source | Référence(s) vers la destination |} Les instructions multi-accès précédentes demandent d'encoder deux adresses dans l'instruction, ainsi que le mode d'adressage pour ces deux adresses. Les instructions sont donc assez longues et ne tiennent pas dans une instruction de taille fixe, des instructions de taille variables sont utilisées. Quelques processeurs fusionnent les instructions de copie mémoire avec les instructions LOAD, STORE et MOV. Elles ont alors une '''instruction mémoire généraliste''', souvent appelée MOV. Elle est capable de faire une lecture, une écriture, une copie entre registres et une copie d'une adresse mémoire vers une autre. Source comme destination doivent encoder le mode d'adressage adéquat, elles n'utilisent pas le même mode d'adressage. Typiquement, le premier opérande désigne la source et l'autre la destination. * Si les deux opérandes sont des registres, le premier registre est copié dans le second. * Si le premier opérande est un registre et l'autre une adresse, c'est une écriture. * Si le premier opérande est une adresse et l'autre un registre, c'est une lecture. * Si les deux opérandes sont des adresses, c'est une copie mémoire. {|class="wikitable" |+ Encodage d'une instruction mémoire généraliste |- ! rowspan="3" | Opcode | rowspan="3" | Mode d'adressage de la source * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) * Adressage immédiat (constante) | rowspan="3" | Mode d'adressage de la destination * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source | Nom de registre destination |- | Adresse mémoire ou registres pour les pointeurs | Adresse mémoire ou registres pour les pointeurs |- | Constante immédiate |} ===Les instructions multi-accès monadiques et dyadiques=== Il existe de rares instructions multi-accès monadiques. Un exemple est celui des instructions de décalage sur les CPU x86. L'opérande à décaler est lue soit depuis les registres, soit depuis la mémoire, elle est écrite au même endroit. Elle fait donc deux accès mémoire, si l'opérande est en RAM : un pour lire d'opérande, l'autre pour écrire le résultat. L'instruction fournit une seule adresse ou un seul numéro de registre, qui sert à la fois pour la source et pour la destination, pour l'opérande et le résultat. Il s'agit donc d'un encodage 1-référence, une sorte d'équivalent de l'encodage 2-référence pour les instructions monadiques. Les instructions monadiques de ce type, qui lisent une opérande en RAM et écrivent un résultat en RAM, au même endroit, porte un nom spécifique. Elles sont mine de rien assez courante. Tous les processeurs modernes en supportent quelques unes. Mais nous ne pouvons pas en parler à ce moment là du cours. La raison est que ces instructions sont toutes des instructions utilisées sur les processeurs multicœurs, dans des contextes très spécifiques, que nous n'avons pas encore abordé. Pour rentrer dans le détail, il s'agit d'une sous-classe de ces fameuses instructions atomiques appelées '''instructions ''read-modify-write'''''. Tout cela deviendra plus clair une fois qu'on en sera au chapitre sur les instructions atomiques. Les processeurs peuvent gérer quelques instructions multi-accès dyadiques. De rares instructions lisent deux opérandes en mémoire, mais le résultat est enregistré dans les registres. Par exemple, sur les processeurs x86, l'instruction de comparaison CMPS peut lire deux opérandes en mémoire RAM et les comparer. Le résultat de la comparaison est mémorisé dans le registre d'état, pas ailleurs, qui est adressé implicitement. L'instruction lit ses opérandes dans la mémoire, pas ailleurs. Enfin, il existe de rares processeurs où les opérandes peuvent être lus depuis les registres ou la RAM, idem pour l'écriture du résultat qui peut se faire dans les registres ou en RAM. De tels processeurs ont tous des instructions de taille variable. En effet, les instructions vont d'instructions qui encodent deux/trois registres, à des instructions encodant deux/trois adresses. Autant les premières sont très courtes, autant les autres sont très longues. La seule solution pratique est d'utiliser des instructions de taille variable. ==Le cas des processeurs x86== Après avoir vu la théorie, nous allons étudier l'encodage des instructions des CPU x86, présents dans nos PC. Ils ont un des encodage les plus complexe qui soit. Ils utilisent des instructions de longueur variable, qui font de 1 à 15 octets. Le jeu d'instruction contient aussi un grand nombre d'instructions, qui n'a eu de cesse d'augmenter avec le temps. Mais surtout, les instructions en question sont assez complexes. Il supporte des instructions cst-reg, reg-reg et ''load-op'', mais aussi quelques instructions multi-accès. Nous allons d'abord voir les instructions les plus complexes, histoire de voir un exemple concret d'instructions multi-accès, avant de voir comment sont encodées les instructions en général. ===L'encodage d'une instruction x86=== L'encodage d'une instruction x86 contient au minimum un opcode, qui peut-être complété avec des informations annexes. Premièrement, il peut être précédé d'un préfixe, qui complète l'opcode. Il modifie l'interprétation de l'opcode ou des octets qui suivent. Il existe un paquet de préfixes très différents dont nous ne pouvons pas encore parler ici. Il y a notamment un préfixe spécifique pour les instructions 64 bits. Suivant le préfixe, l'opcode peut passer de 1 à 3 octets. Il peut y avoir au maximum 4 préfixes, chacun codé sur un octet. Ils sont facultatifs. Deuxièmement, l'opcode peut être suivi par un octet qui précise le mode d'adressage. Il est facultatif car certaines instructions utilisent uniquement le mode d'adressage implicite. Il est appelé l'octet ModR/M. Troisièmement, l'octet ModR/M peut être suivi par trois informations, qui sont utilisées pour calculer l'adresse de l'opérande chargée depuis la mémoire. Elles regroupent un octet SIB pour l'adressage base + indice, un ''offset'', pour les adressages base + ''offset'' et/ou une constante immédiate. Les trois peuvent être combinées si le mode d'adressage des deux opérandes le permet, mais cela demande d'avoir une opérande immédiate couplée à une opérande en mode d'adressage base + indice + ''offset''. Ce n'est pas très courant. {|class="wikitable" |- ! Préfixes !! opcode !! Mod R/M !! SIB !! Displacement !! Immediate |- | Préfixes || opcode || Mode d'adressage || registres pour l'adressage base + indice || ''offset'' || Constante immédiate |- | facultatif || obligatoire || facultatif || facultatif || facultatif |- | 0 à 4 octets || 1, 2, 3 octets, souvent un seul || 1 octet || 1 octet || 1, 2 ou 4 octets || 1, 2, 4 octets en 32 bits, peut aller jusqu'à 8 en 64 bits. |} L'octet SIB inclu deux numéros de registres : un pour le registre de base, un autre pour le registre d'indice. Il inclu aussi la taille de l'opérande, qui est utilisée pour multiplier l'indice, avant de l'additionner au registre de base. {|class="wikitable" |+ Octet SIB |- ! Taille de l'opérande !! Base !! Indice |- | Entier || Numéro de registre || Numéro de registre |- | 2 bits || 3 bits || 3 bits |} L'octet Mod R/M est composé de trois champs, appelés MOD, REG et R/M. MOD est codé sur deux bits et précise le mode d'adressage : * 00 pour l'adressage indirect à registre ou l'adressage base + indice. * 01 pour dire qu'un ''offset'' de 1 octet est présent. * 10 pour dire qu'un ''offset'' de 4 octets est présent. * 11 si les deux opérandes sont dans un registre. Encoder des numéros de registres sur 3 bits limite le processeur à 8 registres. Les CPU x86 s'en sont contenté jusqu’à la génération 32 bits. Lors du passage au 64 bits, 8 autres registres ont été ajouté. Pour ajouter des registres, l'octet SIB se voit ajouter deux bits, un par numéro de registre, ce qui fait qu'il est alors codé sur 10 bits. Mais cela n'est le cas que si l'octet d'opcode est précédé par l'octet de préfixe adéquat, le préfixe REX, VEX ou XOP. Pour les opérations dyadiques, REG encode le numéro de registre de la première opérande, R/M encode le numéro de registre pour la seconde opérande. Pour les opérations à une seule opérande, le champ REG est ignoré et le numéro de registre est dans le champ R/M. En clair, l'octet Mod R/M a une interprétation qui dépend de l'opcode. De plus, la destination est encodée via encodage 2-opérande. La destination est soit la première opérande, soit la seconde. Un bit de l'opcode permet de choisir, ce qui donne un peu de flexibilité à l'encodage 2-opérande. {|class="wikitable" |+ Octet SIB |- ! Taille de l'opérande !! Base !! Indice |- | Entier || Numéro de registre || Numéro de registre |- | 2 bits || 3 bits || 3 bits |} ===Les ''string instructions''=== Les instructions dont nous allons parler sont connues sous le nom d'''instructions de chaines de caractère'', bien qu'elles travaillent en réalité plus sur des tableaux ou des zones de mémoire. Les opérations arithmétiques et logiques ne peuvent pas lire deux opérandes en mémoire, la possibilité est donc limitée à quelques instructions d'accès mémoire bien précises. Les instructions de chaine de caractère peuvent se classer en deux types : celles qui ne font qu'un seul accès mémoire, et celles qui en font deux. Les instructions de chaine de caractère multi-accès sont les suivantes : * L'instruction MOVS copie une adresse mémoire dans une autre, c'est la seule instruction de copie mémoire sur ces CPU. * L'instruction de comparaison CMPS peut lire deux opérandes en mémoire RAM et les comparer. Le résultat de la comparaison est mémorisé dans le registre d'état, pas ailleurs. Les autres instructions de chaine de caractère regroupent trois instructions : * LOD charge une donnée dans le registre EAX, l'adresse de cette donnée est dans le registre ESI. * STO copie une donnée en mémoire RAM, la donnée est dans le registre EAX, l'adresse mémoire est dans le registre ESI. * SCA charge une donnée et la compare avec le registre EAX, l'adresse de la donnée est dans le registre ESI. Pour les échanges entre deux adresses mémoire, les processeurs x86 fournissent une instruction dédiée, appelée MOVS. L'adresse de la source est dans le registre ESI, l'adresse de destination est dans le registre EDI. Vous remarquerez que les registres utilisés sont fixés à l'avance, ce qui fait qu'ils sont adressés implicitement. Les deux registres sont incrémentés/décrémentés à chaque exécution de l'instruction, afin de pointer vers le mot mémoire suivant. Fait intéressant, les instructions mémoires peuvent être répétées automatiquement plusieurs fois, en leur ajoutant un préfixe, un octet placé avant l'opcode. Le nombre de répétitions doit être stocké dans le registre ECX, qui est décrémenté à chaque exécution de l'instruction. Une fois que le registre ECX atteint 0, l'instruction n'est plus répétée et on passe à l'instruction suivante. En clair, l'instruction décrémente automatiquement ce registre à chaque exécution et s'arrête quand il atteint zéro. Suivant le préfixe, d'autres conditions peuvent être ajoutées. Elle peut être répétée plusieurs fois avec l'ajout du préfixe REP. L'instruction avec préfixe, REPMOVS, permet donc de copier un bloc de mémoire dans un autre, de copier N adresses consécutives ! Cela permet de parcourir la mémoire dans l'ordre montant (adresses croissantes) ou descendant (adresses décroissantes), suivant que les registres sont incrémentés ou décrémentés. La direction de parcours de la mémoire est spécifiée dans un bit incorporé dans le processeur, qui vaut 0 (décrémentation) ou 1 (incrémentation), et peut être altéré par des instructions dédiées, de manière à être mis à 1 ou 0. D'autres instructions d'accès mémoire peuvent être préfixées avec REP. Par exemple, l'instruction STOS, qui copie le contenu du registre EAX dans une adresse. On peut exécuter cette instruction une fois, ou la répéter plusieurs fois avec le préfixe REP. Là encore, on doit préciser l'adresse à laquelle écrire dans un registre spécifié, puis le nombre de répétitions dans le registre ECX. Là encore, le nombre de répétitions est décrémenté à chaque exécution, alors que l'adresse est incrémentée/décrémentée. L'instruction REPSTOS est très utile pour mettre à zéro une zone de mémoire, ou pour initialiser un tableau avec des données préétablies. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les modes d'adressage | prevText=Les modes d'adressage | next=Les jeux d'instructions | nextText=Les jeux d'instructions }} </noinclude> a4o61293z43ldv8ruq1xg0ef0f4m98w 773861 773860 2026-10-01T14:08:52Z Mewtow 31375 /* La taille d'une instruction : taille fixe ou variable */ 773861 wikitext text/x-wiki Pour rappel, les programmes informatiques sont placés en mémoire, au même titre que les données qu'ils manipulent. En clair, les instructions sont stockées dans la mémoire RAM/ROM sous la forme de suites de bits, tout comme les données. Il est impossible de faire la différence entre donnée et instruction, vu que rien ne ressemble plus à une suite de bits qu'une autre suite de bits. La seule différence est que les instructions sont chargées via le ''program counter'', alors que les données sont lues ou écrites par des instructions d'accès mémoire. En théorie, cette différence fait que le processeur ne peut pas prendre par erreur des instructions pour des données. ==La taille d'une instruction : taille fixe ou variable== Une instruction est codée sur plusieurs bits. Le nombre de bits utilisé pour coder une instruction est appelée la '''taille de l'instruction'''. Sur certains processeurs, la taille d'une instruction est fixe, c’est-à-dire qu'elle est la même pour toutes les instructions. Mais sur d'autres processeurs, les instructions n'ont pas toutes la même taille, ils gèrent des '''instructions de longueur variable'''. Un exemple de jeu d’instruction à longueur variable est le x86 des pc actuels, où une instruction peut faire entre 1 et 15 octets. L'encodage des instructions x86 est tellement compliqué qu'il prendrait à lui seul plusieurs chapitres ! Les instructions de longueur variable permettent d'économiser un peu de mémoire : avoir des instructions qui font entre 1 et 3 octets est plus avantageux que de tout mettre sur 3 octets. Mais en contrepartie le chargement de l'instruction suivante par le processeur est rendu plus compliqué. Le processeur doit en effet identifier la longueur de l'instruction courante pour savoir où est la suivante. À l'opposé, des instructions de taille fixe gâchent un peu de mémoire, mais permettent au processeur de calculer plus facilement l’adresse de l'instruction suivante et de la charger plus facilement. Il existe des processeurs qui sont un peu entre les deux, avec des instructions de taille fixe, mais qui ne font pas toutes la même taille. Un exemple est jeu d'instruction RISC-V, où il existe des instructions "normales" de 32 bits et des instructions "compressées" de 16 bits. Le processeur charge un mot de 32 bits, ce qui fait qu'il peut lire entre une et deux instructions à la fois. Au tout début de l'instruction, un bit est mis à 0 ou 1 selon que l'instruction soit longue ou courte. Le reste de l'instruction varie suivant sa longueur. Le jeu d'instruction z-architecture, utilisé sur certains processeurs d'IBM, a des instructions de taille variable. Ses instructions font soit 16 bits, soit 32 bits, soit 64 bits. Remarquez que les tailles des instructions sont des puissances de deux, ce qui simplifie grandement l'alignement mémoire. ==L'encodage d'une instruction : généralités== Il est intéressant d'étudier comment les instructions sont encodées, à savoir comment sont organisés ses bits. En effet, une instruction n'est pas qu'une suite de bits sans signification. Elle est en fait découpée en '''champs''', à savoir des suites de bits qui ont une signification. ===L'opcode, aussi appelé "code opération"=== Toute instruction contient un champ opcode, qui indique quelle est l'instruction : est-ce une instruction d'addition, de soustraction, un branchement inconditionnel, un appel de fonction, une lecture en mémoire, etc. Le terme opcode est une abréviation pour "code opération". Typiquement, l'opcode est encodé sur un octet, voire moins. Plus l'opcode est encodé sur un grand nombre de bits, plus le processeur peut supporter un grand nombre d'instruction. Par exemple, un octet d'opcode permet d'encoder 256 instructions différentes, 5 bits d'opcode seulement 32 instructions, etc. Pour la même instruction, l'opcode peut être différent suivant le processeur, ce qui est source d'incompatibilités. Par exemple, les opcodes de l'instruction d'addition ne sont pas les mêmes sur les processeurs x86 (ceux de nos PC) et les anciens macintosh, ou encore les microcontrôleurs. Ce qui fait qu'un opcode de processeur x86 n'aura pas d'équivalent sur un autre processeur, ou correspondra à une instruction totalement différente. En théorie, l'opcode a une taille fixe, ce qui veut dire qu'il est toujours codé sur le même nombre de bits, pour toutes les instructions. Par exemple, sur les processeurs ARM7, l'opcode est encodé sur 8 bits. Il n'y a pas d'instruction qui a un opcode de 5 bits, une autre qui a un opcode de 8 bits, etc. Non, toutes les instructions ont un opcode de 8 bits. Mais il y a des jeux d'instruction qui font exception. Par exemple, sur les processeurs x86, l'opcode peut faire 1, 2 ou 3 octets. Le mécanisme pour gérer des opcodes de taille variable sera expliqué plus tard dans ce chapitre. Il arrive que de rares instructions ne soient composées d'un opcode, sans rien d'autre. Elles ont alors une représentation en binaire qui est unique. : Pour l'anecdote, certains processeurs n'utilisent qu'une seule et unique instruction, et qui peuvent se passer d'opcodes. Mais ce sont des curiosités, pas quelque chose d'utilisé en pratique. ===L'encodage des opérandes et des références=== En plus de l'opcode, une instruction doit préciser où sont ses opérandes. Par opérande, on veut parler des données manipulées par l'instruction. Pour cela, il y a deux cas possibles. * Le premier encode l'opérande dans l'instruction, avec l'adressage immédiat. L'opérande est alors une constante connue à la compilation, elle est intégrée dans l'instruction directement, après l'opcode. * Le second cas indique où se trouve l'opérande, en donnant une référence vers celle-ci. La référence précise dans quel registre ou à quelle adresse se trouve l'opérande. La référence a un champ dédié, rien que pour elle, à la suite de l'opcode. La référence est associée à un mode d'adressage qui précise comment l'interpréter : si c'est un numéro de registre, une adresse mémoire, un pointeur, de quoi calculer l'adresse, etc. Mais laissons de côté le mode d'adressage pour le moment et concentrons-nous sur l'encodage de l'opérande ou de sa référence. Nous allons voir que l'encodage des instructions est compliqué par plusieurs problèmes. Un premier problème est que les instructions n'ont pas le même nombre d'opérandes. Les instructions arithmétiques et logiques sont souvent des '''instructions dyadiques''', à savoir qui prennent deux opérandes et fournissent un résultat. L'addition, la soustraction, les opérations bit à bit ET/OU/XOR, les décalages et rotations, sont dans ce cas. Il existe cependant quelques rares '''instructions monadiques''' qui n'ont qu'une seule opérande et fournissent un résultat, comme les instructions NOT, INC/DEC (incrémentation/décrémentation). Vu qu'elles n'ont pas le même nombre d'opérande, le nombre de bits utilisé pour encoder l'instruction ne sera pas le même. Les opérations d'accès mémoire qui n'ont pas d'opérande à proprement parler, ni même de résultat. Elles doivent cependant savoir quelle adresse elles doivent lire ou écrire. Les instructions LOAD et STORE copient une donnée d'une source vers une destination. Par exemple, l'instruction MOV copie une donnée d'un registre source vers un registre destination, l'instruction LOAD charge une donnée depuis une adresse source vers un registre destination, etc. Et la source comme la destination doivent être encodés dans l'instruction, ce qui est équivalent à encoder deux opérandes. Un second problème, lié au précédent, est que les références n'ont pas toutes la même taille : une adresse utilise plus de bits qu'un numéro de registre, par exemple. En conséquence, une instruction dont les deux références sont des adresses mémoire prendra plus de place qu'un calcul entre deux registres. Les constantes immédiates ont souvent une taille proche de celle des adresses. En effet, les adresses mémoire sont un peu l'équivalent des constantes immédiates, mais pour les instructions d'accès mémoire. La combinaison de ses deux problèmes fait qu'encoder des instructions est compliqué, particulièrement avec des instructions de taille fixe. Les instructions de longueur variables ne sont pas trop gênées par ces deux problèmes. Une instruction de longueur variable prend autant de bits qu'elle a besoin pour encoder ses opérandes et son résultat, ou sa source et sa destination. Une instruction dyadique prendra donc plus de place qu'une instruction monadique. Cependant, elles doivnt faire preuve de mesure et ne pas encoder des instructions super longues. Et c'est très facile d'obtenir deux adresses super longues : une instruction mémoire MOV qui copie une adresse dans une autre, avec deux adresses de 64 bits, peut facilement dépasser les 20 octets ! Les instructions de longueur fixe sont par contre bien plus gênées par les deux problèmes précédents. Elles doivent trouver un moyen pour tout faire rentrer dans 16, 32 ou 64 bits. Une solution systématiquement utilisée est de limiter la taille des adresses et constantes immédiates, qui font généralement 16 bits, rarement plus. Une autre solution, complémentaire de la précédente, est de réduire les possibilités d'adressage. Certaines opérandes sont encodées de manière implicite, on empêche certaines combinaisons d'opérandes, on empêche d’utiliser certains modes d’adressage pour certaines instructions, etc. Leur encodage est donc plus restrictif. Les opérations dyadiques font partie de celles qui subissent des restrictions. Intuitivement, encoder une opération dyadique demande d'encoder trois références : une par opérande, plus une pour le résultat. Le résultat est ce qu'on appelle à tord l'encodage 3-opérandes, en confondant le résultat avec une opérande. Une autre dénomination tout aussi mauvaise est l'encodage encodage 3-adresses car une telle instruction encode 3 adresses, trois noms de registre, etc. Nous parlerons plutôt d''''encodage 3-références''', plus correct, qui indique bien que l’instruction encode trois références. Ce n'est pas parfait car l'une de ces opérandes peut très bien être une constante immédiate, mais passons. Toujours est-il qu'encoder 2 opérandes + 1 résultat prend de la place. Si on prend une instruction encodée sur 32 bits, c'est un petit peu large. Avec un octet par registre et un octet pour l'opcode, on remplit complétement les 32 bits. Et c'est en supposant que le mode d'adressage n'ait pas à être encodé, chose qu'on verra à la section suivante. Par contre, si on souhaite utiliser une constante immédiate, on est fortement limité. Si on prend un processeur avec 16 registres, et un opcode encodé sur un octet, on n'a que 12 bits pour la constante immédiate, ce qui est peu. Heureusement, l'encodage 3-référence peut être modifié pour adresser le résultat de manière implicite, pour économiser un peu de place. Avec cet encodage, le résultat est enregistré dans le registre de la seconde opérande. Pour le dire autrement, l'opérande sera remplacé par le résultat de l'instruction, l'opérande est écrasé. Avec cet encodage, les instructions ne précisent que deux opérandes, pas le résultat, ce qui permet de gagner de la place. On parle alors d''''encodage 2-références''', d'encodage 2-adresses, ou encore d'encodage 2-opérandes. L'encodage 2-références facilite grandement l'usage de l'adressage immédiat. Si on prend une instruction 32 bits, avec un octet d'opcode, cela laisse 24 bits pour encoder un registre et une constante immédiate, ce qui permet d'utiliser des constantes assez larges. Des constantes de 16 bits sont la norme, et on peut atteindre facilement les 20 bits. Mais ce n'est pas leur seul avantage. Un autre avantage est que cela permet de faire tenir des instructions sur seulement 16 bits. Un exemple est celui des processeurs RISC-V, qu'on a mentionné plus haut. Ils ont des instructions 32 bits, et des instructions compressées de 16 bits. Les instructions de 32 bits sont des instructions à trois adresses : elles indiquent deux opérandes et la destination du résultat. Les instructions de 16 bits n'ont que deux opérandes et encodent le résultat de manière implicite. Cela explique qu'elles soient plus courtes : deux opérandes prennent moins de place que trois. [[File:CODE2 formatos.jpg|centre|vignette|upright=2|Exemples d'encodage d'instruction sur une taille fixe. </br> 0 - Opération sans opérande. </br> 1 - Instruction mono-opérande en encodage 1-référence. </br> 2 - Instruction de branchement. </br> 3 - Instruction dyadique en encodage 2-opérande. </br> 4 - Instruction dyadique en encodage 3-opérandes.]] Les instructions monadiques utilisent par défaut l'encodage 2-références : une pour localiser l'opérande, une autre pour indiquer où enregistrer le résultat. Mais elles peuvent aussi utiliser l'encodage une-référence, qui est aux instructions monadiques ce qu'est l'encodage 2-référence aux instructions dyadiques. L'idée est que le résultat écrase l'opérande, il la remplace. Le résultat est écrit à la même adresse que l'opérande, ou il est écrit dans le même registre. L'avantage est qu'on a besoin de ne préciser qu'une seule opérande, pas deux. L'instruction est donc plus courte. ===L'encodage du mode d'adressage=== Il existe deux méthodes principales pour préciser le mode d'adressage utilisé par l'instruction, avec un cas intermédiaire. Dans le premier cas, l'instruction ne gère qu'un mode d'adressage par opérande. Par exemple, prenons le cas d'une instruction COPY qui copie un registre dans un autre. sur les processeurs dit RISC (''Reduced Instruction Set Computer'') , les instructions arithmétiques ne peuvent manipuler que des registres. Un autre cas est celui de l'instruction MOV, qui peut copier un registre ou une constante dans un registre destination. La destination est forcément un registre, pas besoin d'encoder le mode d'adressage pour le registre destination. Par contre, la source doit être encodée explicitement. Dans des cas pareils, le mode d'adressage est déduit automatiquement via l'opcode : on parle de '''mode d'adressage implicite'''. Dans un cas intermédiaire, il se peut que plusieurs instructions existent pour faire la même chose, mais avec des modes d'adressages différents. Le mode d'adressage est alors encodé dans l'opcode, ou en est déduit. Il s'agit d'un cas particulier d'adressage implicite. Un exemple est celui des processeurs ARM, qui ont plusieurs instructions d'addition. Une qui ne manipule que des registres, et adresse trois registres, avec mode adressage implicite. Une autre additionne une constante immédiate avec un registre, ce qui fait que la constante immédiate et les deux registres source/destination sont aussi à mode d'adressage implicite. [[File:Mips32 addi.svg|centre|vignette|upright=2|Exemple d'une instruction avec mode d'adressage implicite, appartenant au jeu d'instruction MIPS.]] Dans le second cas, les instructions gèrent plusieurs modes d'adressage par opérande. Par exemple, une instruction d'addition peut additionner soit deux registres, soit un registre et une adresse, soit un registre et une constante. Dans un cas pareil, l'instruction doit préciser le mode d'adressage utilisé, au moyen de quelques bits intercalés entre l'opcode et les opérandes. On parle de '''mode d'adressage explicite'''. Sur certains processeurs, chaque instruction peut utiliser tous les modes d'adressage supportés par le processeur : on dit que le processeur est orthogonal. [[File:Machine instructions.svg|centre|vignette|upright=2|Exemple d'une instruction avec mode d'adressage explicite.]] Maintenant, tout cela ne vaut que pour encoder le mode d'adressage d'une donnée, pas plus. Mais une instruction gère plusieurs données : ses opérandes, son résultat. Pour les accès mémoire, c'est sa source et de destination. Il faut donc préciser plusieurs modes d'adressage : un par opérande, plus un pour le résultat. Les instructions manipulant plusieurs opérandes peuvent parfois utiliser un mode d'adressage différent pour chaque opérande. Par exemple, une addition manipule deux opérandes, ce qui demande d'utiliser un mode d'adressage par opérande (dans le pire des cas). Il faut donc préciser plusieurs modes d'adressage : un par opérande, plus un pour le résultat. Intuitivement, on se dit qu'il faut utiliser un encodage explicite pour chacune d'entre elles. Idem pour le résultat. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande/résultat est adressée explicitement |- ! Opcode ! Mode d'adressage opérande 1 | Référence vers l'opérande 1 ! Mode d'adressage opérande 2 | Référence vers l'opérande 2 ! Mode d'adressage résultat | Référence vers le résultat |} L'encodage du mode d'adressage prend quelques bits, entre 1 et 4, pas plus. Il est donc intéressant de regrouper les modes d'adressage dans un même octet, dans un même champ. Faire ainsi sépare bien les références/opérandes d'un côté, et les bits de contrôle de l'autre, qui encodent l'instruction proprement dite. Et cela se marie très bien avec les instructions de longueur variable. Elles gardent un cœur de taille fixe auquel on colle des opérandes/références de taille variable. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande/résultat est adressée explicitement |- ! Opcode ! Mode d'adressage opérande 1 ! Mode d'adressage opérande 2 ! Mode d'adressage résultat | Référence vers l'opérande 1 | Référence vers l'opérande 2 | Référence vers le résultat |} Avec l'encodage 2-référence, on n'a pas à encoder explicitement la référence du résultat, ni son mode d'adressage. Cela fait gagner un peu de place. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande est adressée explicitement, le résultat est adressé implicitement |- ! Opcode ! Mode d'adressage opérande 1 ! Mode d'adressage opérande 2/résultat | Référence vers l'opérande 1 | Référence vers l'opérande 2 / le résultat |} Il s'agit là d'un usage de l'encodage implicite du mode d'adressage. Mais il y en a d'autres, comme nous le verrons dans ce qui suit. ===L'encodage de la taille des opérandes=== Enfin, une instruction encode la taille des données qu'elle manipule. C'est surtout utile pour les instructions d'accès mémoire. Il est en théorie possible de se limiter à des instructions mémoire qui lisent/écrivent des données de la même taille que les registres entiers, souvent égal à la taille d'un mot mémoire. Mais dans les faits, seules les architectures anciennes à adressage par mot font ça. De nos jours, les architectures à adressage par byte permettent de préciser qu'on veut lire un entier de 8 bits, 16 bits, 32 bits, 64 bits. Pour cela, la solution la plus simple est d'encoder la taille des données dans l'instruction. On rajoute un champ qui indique quelle est la taille des données à lire/écrire. Le champ n'encode pas un nombre comme 8, 16, 32, 64. À la place, il contient deux bits qui sont à interpréter comme suit : * 00 pour des données de 8 bits ; * 01 pour des données de 16 bits ; * 10 pour des données de 32 bits ; * 11 pour des données de 64 bits. En général, ce champ n'existe que pour les instructions d'accès mémoire, qui sont les seules qui se préoccupent de la taille des opérandes. La taille des données est surtout pertinente quand on lit/écrit en mémoire RAM/ROM. Les opérations arithmétiques sont rarement concernées, car elles manipulent le plus souvent des registres, qui n'ont qu'une seule taille. Il y a des exemples où c'est le cas, comme avec le système d'''alias'' de registres des processeurs x86, mais c'est l'exception plus que la règle. Et de plus, ça ne sert que très rarement de limiter la taille des résultats, surtout qu'une simple opération de masquage peut toujours être employée pour éliminer les bits de poids fort. Quelques jeux d'instructions ajoutent ce système de taille des opérandes pour les opérations arithmétiques simples, comme les additions et soustractions. Il arrive même que certaines opérations gèrent la taille des opérandes, mais pas d'autres, sur certaines jeux d'instruction irréguliers. La raison est une question de compatibilité, par exemple quand un jeu d'instruction de 16 bits a été étendu au 32 bits. Les anciennes instructions gardent leur encodage et travaillent sur 16 bits, d'autres sont ajoutées pour gérer les opérandes 32 bits. Les processeurs x86 utilisent un système totalement différent, avec un pseudo-aliasing des registres, qu'on a vu dans le chapitre sur les registres. ===L'encodage des instructions est avant tout un compromis=== Encoder des instructions est quelque chose d'assez complexe, qui demande de faire un compromis entre des contraintes opposées, contradictoires. Pour savoir ce que doit faire un encodage idéal, faisons une petite liste. Idéalement, un encodage doit utiliser des instructions de taille fixe, afin de simplifier le chargement/décodage des instructions. Avec des instructions de taille variable, le processeur a plus de mal à déterminer où se trouve la prochaine instruction. Il doit déterminer la longueur de l'instruction pour savoir où se trouve la suivante. Alors qu'avec des instructions de taille fixe, le calcul est nettement plus simple, la longueur de l'instruction est connue à l'avance. Les instructions doivent aussi avoir une taille qui est une puissance de deux, afin de simplifier l'alignement des instructions en mémoire. Les instructions peuvent donc faire 8, 16, 32, 64 bits. Et là, voyons chaque taille au cas par cas. * Encoder des instructions sur 8 bits est juste impossible, il n'y a pas assez de bits. Sauf sur des jeux d'instructions spécialisés appelés des machines à pile, qui auront droit à un chapitre dédié. * Encoder des instructions sur 16 bits, c'est possible, mais il faut oublier l'usage de constantes immédiates ou l'adressage absolu, les constantes et adresses ne rentrant pas dans une instruction aussi petite. Concrètement, il faut utiliser des jeux d'instructions spéciaux, comme des architectures à accumulateur, qu'on verra dans quelques chapitres. * Encoder des instructions sur 32 bits est la règle, tous les processeurs font cela de nos jours. Il y a assez de place pour encoder des constantes immédiates et des adresses assez larges, disons 16 ou 24 bits. De même, encoder des instructions trois-opérandes utilisant des registres n'est pas un problème : 32 bits est suffisant pour encoder 4 registres et un opcode. * Encoder des instructions sur 64 bits serait complétement excessif. On aurait assez de place pour mettre plein d'instructions, pleins de registres, des constantes et adresses très larges. Par contre, doubler la taille des instructions signifie doubler la taille du code, les programmes prennent deux fois plus de place qu'avec des instructions 32 bits. Et il faut aussi deux fois plus de débit binaire pour charger le même nombre d'instructions, ce qui implique de doubler la taille du bus qui relie le processeur à la mémoire des instructions. Pour résumer, des instructions de 32 bits sont l'idéal : assez longues pour qu'on puisse encoder des instructions utiles, pas assez longue pour pose problème niveau matériel/mémoire. Il faut un peu pousser pour que ca rentre, si j'ose dire, mais rien d'infaisable. ==Les restrictions sur les modes d'adressage== Intuitivement, on se dit que toute opérande peut utiliser tous les modes d'adressage possibles, il n'y a pas de restrictions particulières. Cependant, ce n'est pas le cas du tout. Permettre à toutes les opérandes d'utiliser tous les modes d'adressage possibles pose quelques problèmes, qu'on ne peut pas résoudre facilement. ===La gestion du mode d'adressage immédiat=== Le premier problème est que de nombreuses combinaisons sont interdites, voire n'ont aucun sens. Par exemple, ça n'aurait aucun sens d'utiliser l'adressage immédiat pour le résultat d'une instruction. De même, ça n'aurait aucun intérêt d'utiliser le mode d'adressage immédiat pour les deux opérandes d'une opération dyadique. Concrètement, le mode d'adressage immédiat ne peut être utilisé que sur un opérande à la fois. De plus, l'adressage immédiat n'a de sens que pour les opérations dyadiques, mais n'a aucun sens avec les opérations monadiques. Une opération sur une constante donnera toujours le même résultat, autant éliminer l'instruction et précalculer ce résultat. En somme, seules les instructions dyadiques peuvent encoder une constante immédiate, pas les autres. Les instructions d'accès mémoire peuvent utiliser une variante de l'adressage immédiat : l'adressage absolu qui encode une adresse directement dans l'instruction. Mais l'encodage est assez similaire. ===Les restrictions de registres source/destination=== Les anciens processeurs avaient des restrictions quant à l'utilisation des registres. Par exemple, certaines instructions ne pouvaient utiliser que certains registres, pas les autres. D'autres avaient un registre de destination prévu à l'avance, d'autres ne pouvaient lire leurs opérandes que dans des registres précis, etc. Voyons cela avec quelques exemples. Les contraintes peuvent porter sur le registre pour le résultat. Un exemple est celui des anciennes architectures MIPS, qui limitaient le registre de résultat pour la multiplication. Les multiplications pouvaient lire leurs opérandes dans tous les registres, mais leur résultat allait dans un registre dédié, séparé des registres généraux, appelé HO/LO. Le registre était en réalité composé de deux registres : le registre LO pour les 32 bits de poids faible du résultat, le registre HO pour les 32 bits de poids fort. Les contraintes peuvent aussi porter sur les opérandes. Elles ne peuvent alors provenir que de certains registres, pas des autres. Et les restrictions peuvent porter sur les deux opérandes, ou alors uniquement sur la première opérande, ou que sur la seconde. Pour donner un exemple, prenons l'exemple des processeurs x86, avec les instructions de décalage. Elle prennent deux opérandes : l'opérande à décaler et le nombre de rangs par lequel il faut décaler. Le nombre de rangs est soit une constante immédiate, soit le registre CL. Les autres registres ne peuvent pas être utilisés pour préciser de combien il faut décaler. La même architecture avait des limitations similaires sur les très anciens processeurs x86, avant l'arrivée du CPU 386. Les opérations de multiplication et d'extension de signe devaient avoir leur première opérande dans le registre AX. De telles contraintes visent à réduire la taille des instructions. Pour reprendre le cas de l'instruction SHL, limiter le nombre de registre opérande à un seul fait qu'on peut l'adresser implicitement. ON n'a pas à encoder le numéro de registre, ce qui économise pas mal de bits. Cela permet à l'instruction de tenir sur un seul octet, deux si l'instruction utilise une constante immédiate. Un autre exemple de contrainte est celui de l'ancien processeur Data General Nova. Il disposait de 4 registres généraux (appelés improprement accumulateurs), mais seul deux d'entre eux servaient pour l'adressage mémoire. Précisément, le processeur gérait trois modes d'adressage, dont l'adressage absolu indicé. Pour rappel, celui-ci additionne une adresse fixe avec un indice variable. L'adresse est intégrée à l'instruction, alors que l'indice est dans un registre. Et sur le Data General Nova, l'indice ne pouvait être que dans les deux derniers registres, pas les deux premiers. ===Les restrictions sur les opérandes mémoire=== Une seconde restriction est liée à la source des opérandes. Un opérande peut provenir de deux sources : des registres, de la mémoire RAM, ou être fournie via adressage immédiat. Le résultat peut lui être enregistré en RAM ou dans les registres. Et cela met en avant un second problème : en faisant cela, une instruction peut effectuer plusieurs accès mémoire. Par exemple, il est possible de lire deux opérandes en mémoire RAM et d'enregistrer le résultat en mémoire RAM. Ou encore de lire deux opérandes en RAM et d'enregistrer le résultat dans les registres. Les processeurs gèrent ce problème de deux manières différentes. La première interdit de faire plusieurs accès mémoire par instruction, ce qui impose des limitations quant à l'utilisation des modes d'adressage. Typiquement, une instruction peut lire un opérande en RAM, mais pas plus. Le jeu d'instruction interdit certaines combinaisons de modes d'adressage avec deux opérandes. La seconde méthode autorise des instructions à faire plusieurs accès mémoire par instruction. Par exemple, une opération dyadique peut lire deux opérandes en RAM, copier une donnée d'une adresse mémoire à une autre, etc. Les modes d'adressages sont moins limités, des combinaisons deviennent possibles. Dans ce qui va suivre, nous allons séparer les instructions en plusieurs types : celles qui ne font pas d'accès mémoire, celles qui en font un, celles qui en font plusieurs. Nous parlerons d''''instruction simple-accès''' pour celles qui font au maximum un accès mémoire. Les instructions qui effectuent plusieurs accès mémoire seront appelées des '''instructions multi-accès'''. Et nous allons les voir séparément. ==L'encodage des instructions simple-accès== Pour commencer, nous allons voir l'encodage des instructions sur les processeurs qui ne gèrent pas plusieurs accès mémoire par instruction. Leur encodage est plus compact que pour les instructions multi-accès. ===Les instructions mémoire simple-accès et leur encodage=== Sur la plupart des processeurs, les instructions ne peuvent faire qu'un seul accès mémoire. Il est interdit de faire plusieurs accès mémoires par instruction. La première conséquence est que les seules instructions d'accès mémoire permises sont : * les instructions LOAD pour charger une donnée de la RAM dans un registre ; * l'instruction STORE pour copier une donnée d'un registre vers la RAM ; * l'instruction MOV en adressage inhérent (registre), qui copie un registre dans un autre ; * l'instruction MOV en adressage immédiat (constante) pour charger une constante immédiate dans un registre destination. D'autres instructions sont possibles, mais elles sont rarement implémentées. Par exemple, on peut citer l'instruction d'échange entre registres XCHG. Une autre possibilité est une variante de l'instruction STORE en mode d'adressage immédiat. Elle copie une constante dans une adresse mémoire. Elle n'est pas présente sur les architectures ARM, POWER PC ou SPARC. Mais laissons-les de côté. Pour rappel, les instructions d'accès mémoire copient une donnée d'une source vers une destination, les deux ayant leur propre mode d'adressage. Il n'est pas systématiquement encodé. Par exemple, l'instruction LOAD charge une donnée dans un registre destination, l'adressage de la destination est donc toujours l'adressage inhérent (nom de registre). Pas besoin d'encoder le mode d'adressage, juste la référence, le nom de registre. Idem pour les autres instructions. {|class="wikitable" |- ! colspan="7" | Instruction LOAD |- | Opcode | Adressage de la source * Adressage absolu (adresse) * Adressage indirect à registre * Adressage base + indice * etc. | Adresse ou référence de la source | | Nom de registre de destination |- ! colspan="7" | Instruction STORE |- | Opcode | | Nom de registre source | Adressage de la destination * Adressage absolu (adresse) * Adressage indirect à registre * Adressage base + indice * etc. | Adresse ou référence de destination |- ! colspan="7" | Instruction MOV |- | rowspan="2" | Opcode | rowspan="2" | * Adressage inhérent (registre) * Adressage immédiat (constante) | Nom de registre source | rowspan="2" | | rowspan="2" | Nom de registre destination |- | Constante immédiate |} Les processeurs x86, et sans doute quelques autres, fusionnent les instructions LOAD, STORE et MOV en une seule instruction appelée MOV. Elle est capable de faire une lecture, une écriture et une copie entre registres. Par contre, elle n'est pas capable de faire une copie d'une adresse mémoire vers une autre. Elles encodent une source et une destination, dont au moins l'une d'entre elle est un registre. Leur encodage place le registre juste après l'opcode, et la seconde opérande après. Un bit de l'instruction dit si la première opérande est la source ou la destination. {|class="wikitable" |+ Encodage d'une instruction MOV généraliste |- | rowspan="3" | Opcode | rowspan="3" | Bit qui indique l'ordre des champs | rowspan="3" | Nom de registre source/destination | rowspan="3" | * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source/destination |- | Adresse mémoire ou registres pour les pointeurs |- | Constante immédiate |} ===Les instructions dyadiques simple-accès et leur encodage=== Les instructions arithmétiques et logique sont elles aussi concernées par la contrainte d'un accès mémoire par instruction. Pour ces opérations, il y a deux possibilités : lire un opérande en RAM, enregistrer le résultat en RAM. Les deux options peuvent s'émuler avec deux instructions consécutives. La première option remplace une instruction LOAD suivie d'une instruction arithmétique/logique, la seconde remplace une instruction arithmétique/logique suivie d'une instruction STORE. Dans les deux cas, on économise un registre : le registre pour charger l'opérande dans le premier cas, le registre pour le résultat dans le second cas. Dans les faits, la seconde option n'est jamais appliquée, le résultat est systématiquement stocké dans un registre. En effet, il est fréquent qu'un opérande soit chargé depuis la mémoire, alors qu'il est rare qu'un résultat soit écrit en mémoire après avoir été calculé. Un résultat calculé a de fortes chances d'être réutilisé par la suite, donc de servir d'opérandes sous peu. Mieux vaut le laisser dans les registres, plutôt que de payer le prix d'un accès mémoire. Lire un opérande en mémoire RAM est par contre beaucoup plus fréquent et donc intéressant. [[File:Isregmem.png|vignette|Instruction dyadique de type ''load-op''.]] Dans ce qui suit, nous parlerons d''''instructions ''load-op''''' pour désigner de telles instructions. Elles lisent un opérande en RAM, mais l'autre opérande est lu depuis les registres et le résultat est enregistré dans les registres. Elles sont opposées aux '''instructions reg-reg''', ce qui est une abréviation pour registre-registre. Ces dernières lisent leurs deux opérandes dans les registres, idem pour l'écriture du résultat. Il faut aussi citer les '''instructions cst-reg''', où un opérande est fourni via adressage immédiat, l'autre opérande est dans les registres. Avec la contrainte d'un seul accès RAM par instruction, ce sont les seuls types d'instruction qui existent. {|class="wikitable" |+ Encodage d'une instruction dyadique reg-reg |- ! rowspan="2" | Opcode | rowspan="2" | * Adressage inhérent (registre) * Adressage immédiat (constante) | Nom de registre pour l'opérande 1 | rowspan="2" | Nom de registre pour l'opérande 2 | rowspan="2" | Nom de registre pour le résultat |- | Constante immédiate |} {|class="wikitable" |+ Encodage d'une instruction ''load-op'' |- ! rowspan="3" | Opcode | rowspan="3" | * Adressage inhérent (registre) * Adressage immédiat (constante) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source | rowspan="3" | | rowspan="3" | Nom de registre destination |- | Constante immédiate |- | Adresse mémoire ou registres pour les pointeurs |} Encoder une instruction ''load-op'' est compliqué. En soit, encoder l'opcode et les numéros de registre ne prend pas beaucoup de place, mais encoder une adresse complète prend beaucoup de bits. Une première solution utilise des instructions de taille variable : les instructions reg-reg sont plus courtes que les instructions ''load-op'', ces dernières prenant plus de place pour coder une adresse. Mais gérer des instructions de taille variable est complexe. Aussi, il existe une solution qui marche avec des instructions de taille fixe, à savoir si toutes les instructions sont codées sur le même nombre d'octets (souvent 2, 4 ou 8 octets selon le processeur) : utiliser l'encodage 2-opérandes. ==Les instructions multi-accès et leur encodage== Passons maintenant aux instructions multi-accès. Nous allons séparer les instructions d'accès mémoire d'un côté, et les instructions dyadiques de l'autre. ===Les instructions mémoire-mémoire=== Les instructions que nous allons voir dans ce qui suit sont des instructions d'accès mémoire, qui sont en plus multi-accès. Elles sont connues sous le nom d''''instruction mémoire-mémoire''', ce qui signifie qu'elle lisent une donnée en mémoire, pour l'enregistrer en mémoire. Les instructions mémoire-mémoire les plus communes sont les '''instructions de copie mémoire''', où une adresse est copiée dans une autre. Les copies en mémoire sont des opérations très fréquentes, il est très fréquent qu'un programme copie un bloc de mémoire dans un autre et beaucoup de programmeurs ont déjà été confronté à un tel cas. Aussi, les processeurs ajoutent des instructions multi-accès pour accélérer ces copies, ce qui fait un bon compromis entre performance et simplicité d'implémentation. Avec elles, la source et la destination sont systématiquement des adresses, mais, il faut préciser leur mode d'adressage : absolu, pointeur, autre. Pour préciser le mode d'adressage, il est possible de préciser un mode d’adressage différent pour la source et de la destination. Par exemple, on peut utiliser le mode d'adressage base + indice pour la source, l'adressage absolu pour la destination. {|class="wikitable" |+ Encodage d'une instruction de copie mémoire |- ! Opcode | Mode d'adressage de la source * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice * etc. | Mode d'adressage de la destination * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice * etc. | Référence(s) vers la source | Référence(s) vers la destination |} Une autre solution utilise le même mode d'adressage pour la source et la destination. On perd en flexibilité, mais ce n'est pas si grave. Les instructions de copie mémoire sont souvent utilisées pour copier des tableaux, qui sont souvent adressés avec l'adressage base + indice ou indirect à registre. Par contre, l'encodage de l'instruction est plus simple, on économise quelques bits. {|class="wikitable" |+ Encodage d'une instruction de copie mémoire |- ! Opcode | Mode d'adressage de la source et de la destination * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice | Référence(s) vers la source | Référence(s) vers la destination |} Les instructions multi-accès précédentes demandent d'encoder deux adresses dans l'instruction, ainsi que le mode d'adressage pour ces deux adresses. Les instructions sont donc assez longues et ne tiennent pas dans une instruction de taille fixe, des instructions de taille variables sont utilisées. Quelques processeurs fusionnent les instructions de copie mémoire avec les instructions LOAD, STORE et MOV. Elles ont alors une '''instruction mémoire généraliste''', souvent appelée MOV. Elle est capable de faire une lecture, une écriture, une copie entre registres et une copie d'une adresse mémoire vers une autre. Source comme destination doivent encoder le mode d'adressage adéquat, elles n'utilisent pas le même mode d'adressage. Typiquement, le premier opérande désigne la source et l'autre la destination. * Si les deux opérandes sont des registres, le premier registre est copié dans le second. * Si le premier opérande est un registre et l'autre une adresse, c'est une écriture. * Si le premier opérande est une adresse et l'autre un registre, c'est une lecture. * Si les deux opérandes sont des adresses, c'est une copie mémoire. {|class="wikitable" |+ Encodage d'une instruction mémoire généraliste |- ! rowspan="3" | Opcode | rowspan="3" | Mode d'adressage de la source * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) * Adressage immédiat (constante) | rowspan="3" | Mode d'adressage de la destination * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source | Nom de registre destination |- | Adresse mémoire ou registres pour les pointeurs | Adresse mémoire ou registres pour les pointeurs |- | Constante immédiate |} ===Les instructions multi-accès monadiques et dyadiques=== Il existe de rares instructions multi-accès monadiques. Un exemple est celui des instructions de décalage sur les CPU x86. L'opérande à décaler est lue soit depuis les registres, soit depuis la mémoire, elle est écrite au même endroit. Elle fait donc deux accès mémoire, si l'opérande est en RAM : un pour lire d'opérande, l'autre pour écrire le résultat. L'instruction fournit une seule adresse ou un seul numéro de registre, qui sert à la fois pour la source et pour la destination, pour l'opérande et le résultat. Il s'agit donc d'un encodage 1-référence, une sorte d'équivalent de l'encodage 2-référence pour les instructions monadiques. Les instructions monadiques de ce type, qui lisent une opérande en RAM et écrivent un résultat en RAM, au même endroit, porte un nom spécifique. Elles sont mine de rien assez courante. Tous les processeurs modernes en supportent quelques unes. Mais nous ne pouvons pas en parler à ce moment là du cours. La raison est que ces instructions sont toutes des instructions utilisées sur les processeurs multicœurs, dans des contextes très spécifiques, que nous n'avons pas encore abordé. Pour rentrer dans le détail, il s'agit d'une sous-classe de ces fameuses instructions atomiques appelées '''instructions ''read-modify-write'''''. Tout cela deviendra plus clair une fois qu'on en sera au chapitre sur les instructions atomiques. Les processeurs peuvent gérer quelques instructions multi-accès dyadiques. De rares instructions lisent deux opérandes en mémoire, mais le résultat est enregistré dans les registres. Par exemple, sur les processeurs x86, l'instruction de comparaison CMPS peut lire deux opérandes en mémoire RAM et les comparer. Le résultat de la comparaison est mémorisé dans le registre d'état, pas ailleurs, qui est adressé implicitement. L'instruction lit ses opérandes dans la mémoire, pas ailleurs. Enfin, il existe de rares processeurs où les opérandes peuvent être lus depuis les registres ou la RAM, idem pour l'écriture du résultat qui peut se faire dans les registres ou en RAM. De tels processeurs ont tous des instructions de taille variable. En effet, les instructions vont d'instructions qui encodent deux/trois registres, à des instructions encodant deux/trois adresses. Autant les premières sont très courtes, autant les autres sont très longues. La seule solution pratique est d'utiliser des instructions de taille variable. ==Le cas des processeurs x86== Après avoir vu la théorie, nous allons étudier l'encodage des instructions des CPU x86, présents dans nos PC. Ils ont un des encodage les plus complexe qui soit. Ils utilisent des instructions de longueur variable, qui font de 1 à 15 octets. Le jeu d'instruction contient aussi un grand nombre d'instructions, qui n'a eu de cesse d'augmenter avec le temps. Mais surtout, les instructions en question sont assez complexes. Il supporte des instructions cst-reg, reg-reg et ''load-op'', mais aussi quelques instructions multi-accès. Nous allons d'abord voir les instructions les plus complexes, histoire de voir un exemple concret d'instructions multi-accès, avant de voir comment sont encodées les instructions en général. ===L'encodage d'une instruction x86=== L'encodage d'une instruction x86 contient au minimum un opcode, qui peut-être complété avec des informations annexes. Premièrement, il peut être précédé d'un préfixe, qui complète l'opcode. Il modifie l'interprétation de l'opcode ou des octets qui suivent. Il existe un paquet de préfixes très différents dont nous ne pouvons pas encore parler ici. Il y a notamment un préfixe spécifique pour les instructions 64 bits. Suivant le préfixe, l'opcode peut passer de 1 à 3 octets. Il peut y avoir au maximum 4 préfixes, chacun codé sur un octet. Ils sont facultatifs. Deuxièmement, l'opcode peut être suivi par un octet qui précise le mode d'adressage. Il est facultatif car certaines instructions utilisent uniquement le mode d'adressage implicite. Il est appelé l'octet ModR/M. Troisièmement, l'octet ModR/M peut être suivi par trois informations, qui sont utilisées pour calculer l'adresse de l'opérande chargée depuis la mémoire. Elles regroupent un octet SIB pour l'adressage base + indice, un ''offset'', pour les adressages base + ''offset'' et/ou une constante immédiate. Les trois peuvent être combinées si le mode d'adressage des deux opérandes le permet, mais cela demande d'avoir une opérande immédiate couplée à une opérande en mode d'adressage base + indice + ''offset''. Ce n'est pas très courant. {|class="wikitable" |- ! Préfixes !! opcode !! Mod R/M !! SIB !! Displacement !! Immediate |- | Préfixes || opcode || Mode d'adressage || registres pour l'adressage base + indice || ''offset'' || Constante immédiate |- | facultatif || obligatoire || facultatif || facultatif || facultatif |- | 0 à 4 octets || 1, 2, 3 octets, souvent un seul || 1 octet || 1 octet || 1, 2 ou 4 octets || 1, 2, 4 octets en 32 bits, peut aller jusqu'à 8 en 64 bits. |} L'octet SIB inclu deux numéros de registres : un pour le registre de base, un autre pour le registre d'indice. Il inclu aussi la taille de l'opérande, qui est utilisée pour multiplier l'indice, avant de l'additionner au registre de base. {|class="wikitable" |+ Octet SIB |- ! Taille de l'opérande !! Base !! Indice |- | Entier || Numéro de registre || Numéro de registre |- | 2 bits || 3 bits || 3 bits |} L'octet Mod R/M est composé de trois champs, appelés MOD, REG et R/M. MOD est codé sur deux bits et précise le mode d'adressage : * 00 pour l'adressage indirect à registre ou l'adressage base + indice. * 01 pour dire qu'un ''offset'' de 1 octet est présent. * 10 pour dire qu'un ''offset'' de 4 octets est présent. * 11 si les deux opérandes sont dans un registre. Encoder des numéros de registres sur 3 bits limite le processeur à 8 registres. Les CPU x86 s'en sont contenté jusqu’à la génération 32 bits. Lors du passage au 64 bits, 8 autres registres ont été ajouté. Pour ajouter des registres, l'octet SIB se voit ajouter deux bits, un par numéro de registre, ce qui fait qu'il est alors codé sur 10 bits. Mais cela n'est le cas que si l'octet d'opcode est précédé par l'octet de préfixe adéquat, le préfixe REX, VEX ou XOP. Pour les opérations dyadiques, REG encode le numéro de registre de la première opérande, R/M encode le numéro de registre pour la seconde opérande. Pour les opérations à une seule opérande, le champ REG est ignoré et le numéro de registre est dans le champ R/M. En clair, l'octet Mod R/M a une interprétation qui dépend de l'opcode. De plus, la destination est encodée via encodage 2-opérande. La destination est soit la première opérande, soit la seconde. Un bit de l'opcode permet de choisir, ce qui donne un peu de flexibilité à l'encodage 2-opérande. {|class="wikitable" |+ Octet SIB |- ! Taille de l'opérande !! Base !! Indice |- | Entier || Numéro de registre || Numéro de registre |- | 2 bits || 3 bits || 3 bits |} ===Les ''string instructions''=== Les instructions dont nous allons parler sont connues sous le nom d'''instructions de chaines de caractère'', bien qu'elles travaillent en réalité plus sur des tableaux ou des zones de mémoire. Les opérations arithmétiques et logiques ne peuvent pas lire deux opérandes en mémoire, la possibilité est donc limitée à quelques instructions d'accès mémoire bien précises. Les instructions de chaine de caractère peuvent se classer en deux types : celles qui ne font qu'un seul accès mémoire, et celles qui en font deux. Les instructions de chaine de caractère multi-accès sont les suivantes : * L'instruction MOVS copie une adresse mémoire dans une autre, c'est la seule instruction de copie mémoire sur ces CPU. * L'instruction de comparaison CMPS peut lire deux opérandes en mémoire RAM et les comparer. Le résultat de la comparaison est mémorisé dans le registre d'état, pas ailleurs. Les autres instructions de chaine de caractère regroupent trois instructions : * LOD charge une donnée dans le registre EAX, l'adresse de cette donnée est dans le registre ESI. * STO copie une donnée en mémoire RAM, la donnée est dans le registre EAX, l'adresse mémoire est dans le registre ESI. * SCA charge une donnée et la compare avec le registre EAX, l'adresse de la donnée est dans le registre ESI. Pour les échanges entre deux adresses mémoire, les processeurs x86 fournissent une instruction dédiée, appelée MOVS. L'adresse de la source est dans le registre ESI, l'adresse de destination est dans le registre EDI. Vous remarquerez que les registres utilisés sont fixés à l'avance, ce qui fait qu'ils sont adressés implicitement. Les deux registres sont incrémentés/décrémentés à chaque exécution de l'instruction, afin de pointer vers le mot mémoire suivant. Fait intéressant, les instructions mémoires peuvent être répétées automatiquement plusieurs fois, en leur ajoutant un préfixe, un octet placé avant l'opcode. Le nombre de répétitions doit être stocké dans le registre ECX, qui est décrémenté à chaque exécution de l'instruction. Une fois que le registre ECX atteint 0, l'instruction n'est plus répétée et on passe à l'instruction suivante. En clair, l'instruction décrémente automatiquement ce registre à chaque exécution et s'arrête quand il atteint zéro. Suivant le préfixe, d'autres conditions peuvent être ajoutées. Elle peut être répétée plusieurs fois avec l'ajout du préfixe REP. L'instruction avec préfixe, REPMOVS, permet donc de copier un bloc de mémoire dans un autre, de copier N adresses consécutives ! Cela permet de parcourir la mémoire dans l'ordre montant (adresses croissantes) ou descendant (adresses décroissantes), suivant que les registres sont incrémentés ou décrémentés. La direction de parcours de la mémoire est spécifiée dans un bit incorporé dans le processeur, qui vaut 0 (décrémentation) ou 1 (incrémentation), et peut être altéré par des instructions dédiées, de manière à être mis à 1 ou 0. D'autres instructions d'accès mémoire peuvent être préfixées avec REP. Par exemple, l'instruction STOS, qui copie le contenu du registre EAX dans une adresse. On peut exécuter cette instruction une fois, ou la répéter plusieurs fois avec le préfixe REP. Là encore, on doit préciser l'adresse à laquelle écrire dans un registre spécifié, puis le nombre de répétitions dans le registre ECX. Là encore, le nombre de répétitions est décrémenté à chaque exécution, alors que l'adresse est incrémentée/décrémentée. L'instruction REPSTOS est très utile pour mettre à zéro une zone de mémoire, ou pour initialiser un tableau avec des données préétablies. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les modes d'adressage | prevText=Les modes d'adressage | next=Les jeux d'instructions | nextText=Les jeux d'instructions }} </noinclude> dm7lzlzdsxfwo3taoc1a0ua83yuybx7 773862 773861 2026-10-01T14:14:06Z Mewtow 31375 /* L'encodage des opérandes et des références */ 773862 wikitext text/x-wiki Pour rappel, les programmes informatiques sont placés en mémoire, au même titre que les données qu'ils manipulent. En clair, les instructions sont stockées dans la mémoire RAM/ROM sous la forme de suites de bits, tout comme les données. Il est impossible de faire la différence entre donnée et instruction, vu que rien ne ressemble plus à une suite de bits qu'une autre suite de bits. La seule différence est que les instructions sont chargées via le ''program counter'', alors que les données sont lues ou écrites par des instructions d'accès mémoire. En théorie, cette différence fait que le processeur ne peut pas prendre par erreur des instructions pour des données. ==La taille d'une instruction : taille fixe ou variable== Une instruction est codée sur plusieurs bits. Le nombre de bits utilisé pour coder une instruction est appelée la '''taille de l'instruction'''. Sur certains processeurs, la taille d'une instruction est fixe, c’est-à-dire qu'elle est la même pour toutes les instructions. Mais sur d'autres processeurs, les instructions n'ont pas toutes la même taille, ils gèrent des '''instructions de longueur variable'''. Un exemple de jeu d’instruction à longueur variable est le x86 des pc actuels, où une instruction peut faire entre 1 et 15 octets. L'encodage des instructions x86 est tellement compliqué qu'il prendrait à lui seul plusieurs chapitres ! Les instructions de longueur variable permettent d'économiser un peu de mémoire : avoir des instructions qui font entre 1 et 3 octets est plus avantageux que de tout mettre sur 3 octets. Mais en contrepartie le chargement de l'instruction suivante par le processeur est rendu plus compliqué. Le processeur doit en effet identifier la longueur de l'instruction courante pour savoir où est la suivante. À l'opposé, des instructions de taille fixe gâchent un peu de mémoire, mais permettent au processeur de calculer plus facilement l’adresse de l'instruction suivante et de la charger plus facilement. Il existe des processeurs qui sont un peu entre les deux, avec des instructions de taille fixe, mais qui ne font pas toutes la même taille. Un exemple est jeu d'instruction RISC-V, où il existe des instructions "normales" de 32 bits et des instructions "compressées" de 16 bits. Le processeur charge un mot de 32 bits, ce qui fait qu'il peut lire entre une et deux instructions à la fois. Au tout début de l'instruction, un bit est mis à 0 ou 1 selon que l'instruction soit longue ou courte. Le reste de l'instruction varie suivant sa longueur. Le jeu d'instruction z-architecture, utilisé sur certains processeurs d'IBM, a des instructions de taille variable. Ses instructions font soit 16 bits, soit 32 bits, soit 64 bits. Remarquez que les tailles des instructions sont des puissances de deux, ce qui simplifie grandement l'alignement mémoire. ==L'encodage d'une instruction : généralités== Il est intéressant d'étudier comment les instructions sont encodées, à savoir comment sont organisés ses bits. En effet, une instruction n'est pas qu'une suite de bits sans signification. Elle est en fait découpée en '''champs''', à savoir des suites de bits qui ont une signification. ===L'opcode, aussi appelé "code opération"=== Toute instruction contient un champ opcode, qui indique quelle est l'instruction : est-ce une instruction d'addition, de soustraction, un branchement inconditionnel, un appel de fonction, une lecture en mémoire, etc. Le terme opcode est une abréviation pour "code opération". Typiquement, l'opcode est encodé sur un octet, voire moins. Plus l'opcode est encodé sur un grand nombre de bits, plus le processeur peut supporter un grand nombre d'instruction. Par exemple, un octet d'opcode permet d'encoder 256 instructions différentes, 5 bits d'opcode seulement 32 instructions, etc. Pour la même instruction, l'opcode peut être différent suivant le processeur, ce qui est source d'incompatibilités. Par exemple, les opcodes de l'instruction d'addition ne sont pas les mêmes sur les processeurs x86 (ceux de nos PC) et les anciens macintosh, ou encore les microcontrôleurs. Ce qui fait qu'un opcode de processeur x86 n'aura pas d'équivalent sur un autre processeur, ou correspondra à une instruction totalement différente. En théorie, l'opcode a une taille fixe, ce qui veut dire qu'il est toujours codé sur le même nombre de bits, pour toutes les instructions. Par exemple, sur les processeurs ARM7, l'opcode est encodé sur 8 bits. Il n'y a pas d'instruction qui a un opcode de 5 bits, une autre qui a un opcode de 8 bits, etc. Non, toutes les instructions ont un opcode de 8 bits. Mais il y a des jeux d'instruction qui font exception. Par exemple, sur les processeurs x86, l'opcode peut faire 1, 2 ou 3 octets. Le mécanisme pour gérer des opcodes de taille variable sera expliqué plus tard dans ce chapitre. Il arrive que de rares instructions ne soient composées d'un opcode, sans rien d'autre. Elles ont alors une représentation en binaire qui est unique. : Pour l'anecdote, certains processeurs n'utilisent qu'une seule et unique instruction, et qui peuvent se passer d'opcodes. Mais ce sont des curiosités, pas quelque chose d'utilisé en pratique. ===L'encodage des opérandes et des références=== En plus de l'opcode, une instruction doit préciser où sont ses opérandes. Par opérande, on veut parler des données manipulées par l'instruction. Pour cela, il y a deux cas possibles. * Le premier encode l'opérande dans l'instruction, avec l'adressage immédiat. L'opérande est alors une constante connue à la compilation, elle est intégrée dans l'instruction directement, après l'opcode. * Le second cas indique où se trouve l'opérande, en donnant une référence vers celle-ci. La référence précise dans quel registre ou à quelle adresse se trouve l'opérande. La référence a un champ dédié, rien que pour elle, à la suite de l'opcode. La référence est associée à un mode d'adressage qui précise comment l'interpréter : si c'est un numéro de registre, une adresse mémoire, un pointeur, de quoi calculer l'adresse, etc. Mais laissons de côté le mode d'adressage pour le moment et concentrons-nous sur l'encodage de l'opérande ou de sa référence. Nous allons voir que l'encodage des instructions est compliqué par plusieurs problèmes. Un premier problème est que les instructions n'ont pas le même nombre d'opérandes. Les instructions arithmétiques et logiques sont souvent des '''instructions dyadiques''', à savoir qui prennent deux opérandes et fournissent un résultat. L'addition, la soustraction, les opérations bit à bit ET/OU/XOR, les décalages et rotations, sont dans ce cas. Il existe cependant quelques rares '''instructions monadiques''' qui n'ont qu'une seule opérande et fournissent un résultat, comme les instructions NOT, INC/DEC (incrémentation/décrémentation). Vu qu'elles n'ont pas le même nombre d'opérande, le nombre de bits utilisé pour encoder l'instruction ne sera pas le même. Les opérations d'accès mémoire qui n'ont pas d'opérande à proprement parler, ni même de résultat. Elles doivent cependant savoir quelle adresse elles doivent lire ou écrire. Les instructions LOAD et STORE copient une donnée d'une source vers une destination. Par exemple, l'instruction MOV copie une donnée d'un registre source vers un registre destination, l'instruction LOAD charge une donnée depuis une adresse source vers un registre destination, etc. Et la source comme la destination doivent être encodés dans l'instruction, ce qui est équivalent à encoder deux opérandes. Un second problème, lié au précédent, est que les références n'ont pas toutes la même taille : une adresse utilise plus de bits qu'un numéro de registre, par exemple. En conséquence, une instruction dont les deux références sont des adresses mémoire prendra plus de place qu'un calcul entre deux registres. Les constantes immédiates ont souvent une taille proche de celle des adresses. En effet, les adresses mémoire sont un peu l'équivalent des constantes immédiates, mais pour les instructions d'accès mémoire. La combinaison de ses deux problèmes fait qu'encoder des instructions est compliqué, particulièrement avec des instructions de taille fixe. Les instructions de longueur variables ne sont pas trop gênées par ces deux problèmes. Une instruction de longueur variable prend autant de bits qu'elle a besoin pour encoder ses opérandes et son résultat, ou sa source et sa destination. Une instruction dyadique prendra donc plus de place qu'une instruction monadique. Cependant, elles doivnt faire preuve de mesure et ne pas encoder des instructions super longues. Et c'est très facile d'obtenir deux adresses super longues : une instruction mémoire MOV qui copie une adresse dans une autre, avec deux adresses de 64 bits, peut facilement dépasser les 20 octets ! Les instructions de longueur fixe sont par contre bien plus gênées par les deux problèmes précédents. Elles doivent trouver un moyen pour tout faire rentrer dans 16, 32 ou 64 bits. Une solution systématiquement utilisée est de limiter la taille des adresses et constantes immédiates, qui font généralement 16 bits, rarement plus. Une autre solution, complémentaire de la précédente, est de réduire les possibilités d'adressage. Certaines opérandes sont encodées de manière implicite, on empêche certaines combinaisons d'opérandes, on empêche d’utiliser certains modes d’adressage pour certaines instructions, etc. Leur encodage est donc plus restrictif. Les opérations dyadiques font partie de celles qui subissent des restrictions. Intuitivement, encoder une opération dyadique demande d'encoder trois références : une par opérande, plus une pour le résultat. Le résultat est ce qu'on appelle à tord l'encodage 3-opérandes, en confondant le résultat avec une opérande. Une autre dénomination tout aussi mauvaise est l'encodage encodage 3-adresses car une telle instruction encode 3 adresses, trois noms de registre, etc. Nous parlerons plutôt d''''encodage 3-références''', plus correct, qui indique bien que l’instruction encode trois références. Ce n'est pas parfait car l'une de ces opérandes peut très bien être une constante immédiate, mais passons. Toujours est-il qu'encoder 2 opérandes + 1 résultat prend de la place. Si on prend une instruction encodée sur 32 bits, c'est un petit peu large. Avec un octet par registre et un octet pour l'opcode, on remplit complétement les 32 bits. Et c'est en supposant que le mode d'adressage n'ait pas à être encodé, chose qu'on verra à la section suivante. Par contre, si on souhaite utiliser une constante immédiate, on est fortement limité. Si on prend un processeur avec 16 registres, et un opcode encodé sur un octet, on n'a que 12 bits pour la constante immédiate, ce qui est peu. Heureusement, l'encodage 3-référence peut être modifié pour adresser le résultat de manière implicite, pour économiser un peu de place. Avec cet encodage, le résultat est enregistré dans le registre de la seconde opérande. Pour le dire autrement, l'opérande sera remplacé par le résultat de l'instruction, l'opérande est écrasé. Avec cet encodage, les instructions ne précisent que deux opérandes, pas le résultat, ce qui permet de gagner de la place. On parle alors d''''encodage 2-références''', d'encodage 2-adresses, ou encore d'encodage 2-opérandes. L'encodage 2-références facilite grandement l'usage de l'adressage immédiat. Si on prend une instruction 32 bits, avec un octet d'opcode, cela laisse 24 bits pour encoder un registre et une constante immédiate, ce qui permet d'utiliser des constantes assez larges. Des constantes de 16 bits sont la norme, et on peut atteindre facilement les 20 bits. Mais ce n'est pas leur seul avantage. Un autre avantage est que cela permet de faire tenir des instructions sur seulement 16 bits. Un exemple est celui des processeurs RISC-V, qu'on a mentionné plus haut. Ils ont des instructions 32 bits, et des instructions compressées de 16 bits. Les instructions de 32 bits sont des instructions à trois adresses : elles indiquent deux opérandes et la destination du résultat. Les instructions de 16 bits n'ont que deux opérandes et encodent le résultat de manière implicite. Cela explique qu'elles soient plus courtes : deux opérandes prennent moins de place que trois. [[File:CODE2 formatos.jpg|centre|vignette|upright=2|Exemples d'encodage d'instruction sur une taille fixe. </br> 0 - Opération sans opérande. </br> 1 - Instruction mono-opérande en encodage 1-référence. </br> 2 - Instruction de branchement. </br> 3 - Instruction dyadique en encodage 2-opérande. </br> 4 - Instruction dyadique en encodage 3-opérandes.]] Les instructions monadiques utilisent par défaut l'encodage 2-références : une pour localiser l'opérande, une autre pour indiquer où enregistrer le résultat. Mais elles peuvent aussi utiliser l''''encodage une-référence''', qui est aux instructions monadiques ce qu'est l'encodage 2-référence aux instructions dyadiques. L'idée est que le résultat écrase l'opérande, il la remplace. Le résultat est écrit à la même adresse que l'opérande, ou il est écrit dans le même registre. L'avantage est qu'on a besoin de ne préciser qu'une seule opérande, pas deux. L'instruction est donc plus courte. L'encodage une-référence est assez naturel pour certaines instructions. Par exemple, prenons les instructions INC et DEC, qui incrémentent ou décrémentent un registre. Généralement, quand on incrémente une valeur, la valeur est modifiée directement. Il parait naturel que le résultat remplace l'opérande, les deux restent dans le même registre. Il n'y a donc pas besoin de préciser le registre pour le résultat, ce qui impose l'encodage une-référence. ===L'encodage du mode d'adressage=== Il existe deux méthodes principales pour préciser le mode d'adressage utilisé par l'instruction, avec un cas intermédiaire. Dans le premier cas, l'instruction ne gère qu'un mode d'adressage par opérande. Par exemple, prenons le cas d'une instruction COPY qui copie un registre dans un autre. sur les processeurs dit RISC (''Reduced Instruction Set Computer'') , les instructions arithmétiques ne peuvent manipuler que des registres. Un autre cas est celui de l'instruction MOV, qui peut copier un registre ou une constante dans un registre destination. La destination est forcément un registre, pas besoin d'encoder le mode d'adressage pour le registre destination. Par contre, la source doit être encodée explicitement. Dans des cas pareils, le mode d'adressage est déduit automatiquement via l'opcode : on parle de '''mode d'adressage implicite'''. Dans un cas intermédiaire, il se peut que plusieurs instructions existent pour faire la même chose, mais avec des modes d'adressages différents. Le mode d'adressage est alors encodé dans l'opcode, ou en est déduit. Il s'agit d'un cas particulier d'adressage implicite. Un exemple est celui des processeurs ARM, qui ont plusieurs instructions d'addition. Une qui ne manipule que des registres, et adresse trois registres, avec mode adressage implicite. Une autre additionne une constante immédiate avec un registre, ce qui fait que la constante immédiate et les deux registres source/destination sont aussi à mode d'adressage implicite. [[File:Mips32 addi.svg|centre|vignette|upright=2|Exemple d'une instruction avec mode d'adressage implicite, appartenant au jeu d'instruction MIPS.]] Dans le second cas, les instructions gèrent plusieurs modes d'adressage par opérande. Par exemple, une instruction d'addition peut additionner soit deux registres, soit un registre et une adresse, soit un registre et une constante. Dans un cas pareil, l'instruction doit préciser le mode d'adressage utilisé, au moyen de quelques bits intercalés entre l'opcode et les opérandes. On parle de '''mode d'adressage explicite'''. Sur certains processeurs, chaque instruction peut utiliser tous les modes d'adressage supportés par le processeur : on dit que le processeur est orthogonal. [[File:Machine instructions.svg|centre|vignette|upright=2|Exemple d'une instruction avec mode d'adressage explicite.]] Maintenant, tout cela ne vaut que pour encoder le mode d'adressage d'une donnée, pas plus. Mais une instruction gère plusieurs données : ses opérandes, son résultat. Pour les accès mémoire, c'est sa source et de destination. Il faut donc préciser plusieurs modes d'adressage : un par opérande, plus un pour le résultat. Les instructions manipulant plusieurs opérandes peuvent parfois utiliser un mode d'adressage différent pour chaque opérande. Par exemple, une addition manipule deux opérandes, ce qui demande d'utiliser un mode d'adressage par opérande (dans le pire des cas). Il faut donc préciser plusieurs modes d'adressage : un par opérande, plus un pour le résultat. Intuitivement, on se dit qu'il faut utiliser un encodage explicite pour chacune d'entre elles. Idem pour le résultat. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande/résultat est adressée explicitement |- ! Opcode ! Mode d'adressage opérande 1 | Référence vers l'opérande 1 ! Mode d'adressage opérande 2 | Référence vers l'opérande 2 ! Mode d'adressage résultat | Référence vers le résultat |} L'encodage du mode d'adressage prend quelques bits, entre 1 et 4, pas plus. Il est donc intéressant de regrouper les modes d'adressage dans un même octet, dans un même champ. Faire ainsi sépare bien les références/opérandes d'un côté, et les bits de contrôle de l'autre, qui encodent l'instruction proprement dite. Et cela se marie très bien avec les instructions de longueur variable. Elles gardent un cœur de taille fixe auquel on colle des opérandes/références de taille variable. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande/résultat est adressée explicitement |- ! Opcode ! Mode d'adressage opérande 1 ! Mode d'adressage opérande 2 ! Mode d'adressage résultat | Référence vers l'opérande 1 | Référence vers l'opérande 2 | Référence vers le résultat |} Avec l'encodage 2-référence, on n'a pas à encoder explicitement la référence du résultat, ni son mode d'adressage. Cela fait gagner un peu de place. {|class="wikitable" |+ Encodage d'une instruction où chaque opérande est adressée explicitement, le résultat est adressé implicitement |- ! Opcode ! Mode d'adressage opérande 1 ! Mode d'adressage opérande 2/résultat | Référence vers l'opérande 1 | Référence vers l'opérande 2 / le résultat |} Il s'agit là d'un usage de l'encodage implicite du mode d'adressage. Mais il y en a d'autres, comme nous le verrons dans ce qui suit. ===L'encodage de la taille des opérandes=== Enfin, une instruction encode la taille des données qu'elle manipule. C'est surtout utile pour les instructions d'accès mémoire. Il est en théorie possible de se limiter à des instructions mémoire qui lisent/écrivent des données de la même taille que les registres entiers, souvent égal à la taille d'un mot mémoire. Mais dans les faits, seules les architectures anciennes à adressage par mot font ça. De nos jours, les architectures à adressage par byte permettent de préciser qu'on veut lire un entier de 8 bits, 16 bits, 32 bits, 64 bits. Pour cela, la solution la plus simple est d'encoder la taille des données dans l'instruction. On rajoute un champ qui indique quelle est la taille des données à lire/écrire. Le champ n'encode pas un nombre comme 8, 16, 32, 64. À la place, il contient deux bits qui sont à interpréter comme suit : * 00 pour des données de 8 bits ; * 01 pour des données de 16 bits ; * 10 pour des données de 32 bits ; * 11 pour des données de 64 bits. En général, ce champ n'existe que pour les instructions d'accès mémoire, qui sont les seules qui se préoccupent de la taille des opérandes. La taille des données est surtout pertinente quand on lit/écrit en mémoire RAM/ROM. Les opérations arithmétiques sont rarement concernées, car elles manipulent le plus souvent des registres, qui n'ont qu'une seule taille. Il y a des exemples où c'est le cas, comme avec le système d'''alias'' de registres des processeurs x86, mais c'est l'exception plus que la règle. Et de plus, ça ne sert que très rarement de limiter la taille des résultats, surtout qu'une simple opération de masquage peut toujours être employée pour éliminer les bits de poids fort. Quelques jeux d'instructions ajoutent ce système de taille des opérandes pour les opérations arithmétiques simples, comme les additions et soustractions. Il arrive même que certaines opérations gèrent la taille des opérandes, mais pas d'autres, sur certaines jeux d'instruction irréguliers. La raison est une question de compatibilité, par exemple quand un jeu d'instruction de 16 bits a été étendu au 32 bits. Les anciennes instructions gardent leur encodage et travaillent sur 16 bits, d'autres sont ajoutées pour gérer les opérandes 32 bits. Les processeurs x86 utilisent un système totalement différent, avec un pseudo-aliasing des registres, qu'on a vu dans le chapitre sur les registres. ===L'encodage des instructions est avant tout un compromis=== Encoder des instructions est quelque chose d'assez complexe, qui demande de faire un compromis entre des contraintes opposées, contradictoires. Pour savoir ce que doit faire un encodage idéal, faisons une petite liste. Idéalement, un encodage doit utiliser des instructions de taille fixe, afin de simplifier le chargement/décodage des instructions. Avec des instructions de taille variable, le processeur a plus de mal à déterminer où se trouve la prochaine instruction. Il doit déterminer la longueur de l'instruction pour savoir où se trouve la suivante. Alors qu'avec des instructions de taille fixe, le calcul est nettement plus simple, la longueur de l'instruction est connue à l'avance. Les instructions doivent aussi avoir une taille qui est une puissance de deux, afin de simplifier l'alignement des instructions en mémoire. Les instructions peuvent donc faire 8, 16, 32, 64 bits. Et là, voyons chaque taille au cas par cas. * Encoder des instructions sur 8 bits est juste impossible, il n'y a pas assez de bits. Sauf sur des jeux d'instructions spécialisés appelés des machines à pile, qui auront droit à un chapitre dédié. * Encoder des instructions sur 16 bits, c'est possible, mais il faut oublier l'usage de constantes immédiates ou l'adressage absolu, les constantes et adresses ne rentrant pas dans une instruction aussi petite. Concrètement, il faut utiliser des jeux d'instructions spéciaux, comme des architectures à accumulateur, qu'on verra dans quelques chapitres. * Encoder des instructions sur 32 bits est la règle, tous les processeurs font cela de nos jours. Il y a assez de place pour encoder des constantes immédiates et des adresses assez larges, disons 16 ou 24 bits. De même, encoder des instructions trois-opérandes utilisant des registres n'est pas un problème : 32 bits est suffisant pour encoder 4 registres et un opcode. * Encoder des instructions sur 64 bits serait complétement excessif. On aurait assez de place pour mettre plein d'instructions, pleins de registres, des constantes et adresses très larges. Par contre, doubler la taille des instructions signifie doubler la taille du code, les programmes prennent deux fois plus de place qu'avec des instructions 32 bits. Et il faut aussi deux fois plus de débit binaire pour charger le même nombre d'instructions, ce qui implique de doubler la taille du bus qui relie le processeur à la mémoire des instructions. Pour résumer, des instructions de 32 bits sont l'idéal : assez longues pour qu'on puisse encoder des instructions utiles, pas assez longue pour pose problème niveau matériel/mémoire. Il faut un peu pousser pour que ca rentre, si j'ose dire, mais rien d'infaisable. ==Les restrictions sur les modes d'adressage== Intuitivement, on se dit que toute opérande peut utiliser tous les modes d'adressage possibles, il n'y a pas de restrictions particulières. Cependant, ce n'est pas le cas du tout. Permettre à toutes les opérandes d'utiliser tous les modes d'adressage possibles pose quelques problèmes, qu'on ne peut pas résoudre facilement. ===La gestion du mode d'adressage immédiat=== Le premier problème est que de nombreuses combinaisons sont interdites, voire n'ont aucun sens. Par exemple, ça n'aurait aucun sens d'utiliser l'adressage immédiat pour le résultat d'une instruction. De même, ça n'aurait aucun intérêt d'utiliser le mode d'adressage immédiat pour les deux opérandes d'une opération dyadique. Concrètement, le mode d'adressage immédiat ne peut être utilisé que sur un opérande à la fois. De plus, l'adressage immédiat n'a de sens que pour les opérations dyadiques, mais n'a aucun sens avec les opérations monadiques. Une opération sur une constante donnera toujours le même résultat, autant éliminer l'instruction et précalculer ce résultat. En somme, seules les instructions dyadiques peuvent encoder une constante immédiate, pas les autres. Les instructions d'accès mémoire peuvent utiliser une variante de l'adressage immédiat : l'adressage absolu qui encode une adresse directement dans l'instruction. Mais l'encodage est assez similaire. ===Les restrictions de registres source/destination=== Les anciens processeurs avaient des restrictions quant à l'utilisation des registres. Par exemple, certaines instructions ne pouvaient utiliser que certains registres, pas les autres. D'autres avaient un registre de destination prévu à l'avance, d'autres ne pouvaient lire leurs opérandes que dans des registres précis, etc. Voyons cela avec quelques exemples. Les contraintes peuvent porter sur le registre pour le résultat. Un exemple est celui des anciennes architectures MIPS, qui limitaient le registre de résultat pour la multiplication. Les multiplications pouvaient lire leurs opérandes dans tous les registres, mais leur résultat allait dans un registre dédié, séparé des registres généraux, appelé HO/LO. Le registre était en réalité composé de deux registres : le registre LO pour les 32 bits de poids faible du résultat, le registre HO pour les 32 bits de poids fort. Les contraintes peuvent aussi porter sur les opérandes. Elles ne peuvent alors provenir que de certains registres, pas des autres. Et les restrictions peuvent porter sur les deux opérandes, ou alors uniquement sur la première opérande, ou que sur la seconde. Pour donner un exemple, prenons l'exemple des processeurs x86, avec les instructions de décalage. Elle prennent deux opérandes : l'opérande à décaler et le nombre de rangs par lequel il faut décaler. Le nombre de rangs est soit une constante immédiate, soit le registre CL. Les autres registres ne peuvent pas être utilisés pour préciser de combien il faut décaler. La même architecture avait des limitations similaires sur les très anciens processeurs x86, avant l'arrivée du CPU 386. Les opérations de multiplication et d'extension de signe devaient avoir leur première opérande dans le registre AX. De telles contraintes visent à réduire la taille des instructions. Pour reprendre le cas de l'instruction SHL, limiter le nombre de registre opérande à un seul fait qu'on peut l'adresser implicitement. ON n'a pas à encoder le numéro de registre, ce qui économise pas mal de bits. Cela permet à l'instruction de tenir sur un seul octet, deux si l'instruction utilise une constante immédiate. Un autre exemple de contrainte est celui de l'ancien processeur Data General Nova. Il disposait de 4 registres généraux (appelés improprement accumulateurs), mais seul deux d'entre eux servaient pour l'adressage mémoire. Précisément, le processeur gérait trois modes d'adressage, dont l'adressage absolu indicé. Pour rappel, celui-ci additionne une adresse fixe avec un indice variable. L'adresse est intégrée à l'instruction, alors que l'indice est dans un registre. Et sur le Data General Nova, l'indice ne pouvait être que dans les deux derniers registres, pas les deux premiers. ===Les restrictions sur les opérandes mémoire=== Une seconde restriction est liée à la source des opérandes. Un opérande peut provenir de deux sources : des registres, de la mémoire RAM, ou être fournie via adressage immédiat. Le résultat peut lui être enregistré en RAM ou dans les registres. Et cela met en avant un second problème : en faisant cela, une instruction peut effectuer plusieurs accès mémoire. Par exemple, il est possible de lire deux opérandes en mémoire RAM et d'enregistrer le résultat en mémoire RAM. Ou encore de lire deux opérandes en RAM et d'enregistrer le résultat dans les registres. Les processeurs gèrent ce problème de deux manières différentes. La première interdit de faire plusieurs accès mémoire par instruction, ce qui impose des limitations quant à l'utilisation des modes d'adressage. Typiquement, une instruction peut lire un opérande en RAM, mais pas plus. Le jeu d'instruction interdit certaines combinaisons de modes d'adressage avec deux opérandes. La seconde méthode autorise des instructions à faire plusieurs accès mémoire par instruction. Par exemple, une opération dyadique peut lire deux opérandes en RAM, copier une donnée d'une adresse mémoire à une autre, etc. Les modes d'adressages sont moins limités, des combinaisons deviennent possibles. Dans ce qui va suivre, nous allons séparer les instructions en plusieurs types : celles qui ne font pas d'accès mémoire, celles qui en font un, celles qui en font plusieurs. Nous parlerons d''''instruction simple-accès''' pour celles qui font au maximum un accès mémoire. Les instructions qui effectuent plusieurs accès mémoire seront appelées des '''instructions multi-accès'''. Et nous allons les voir séparément. ==L'encodage des instructions simple-accès== Pour commencer, nous allons voir l'encodage des instructions sur les processeurs qui ne gèrent pas plusieurs accès mémoire par instruction. Leur encodage est plus compact que pour les instructions multi-accès. ===Les instructions mémoire simple-accès et leur encodage=== Sur la plupart des processeurs, les instructions ne peuvent faire qu'un seul accès mémoire. Il est interdit de faire plusieurs accès mémoires par instruction. La première conséquence est que les seules instructions d'accès mémoire permises sont : * les instructions LOAD pour charger une donnée de la RAM dans un registre ; * l'instruction STORE pour copier une donnée d'un registre vers la RAM ; * l'instruction MOV en adressage inhérent (registre), qui copie un registre dans un autre ; * l'instruction MOV en adressage immédiat (constante) pour charger une constante immédiate dans un registre destination. D'autres instructions sont possibles, mais elles sont rarement implémentées. Par exemple, on peut citer l'instruction d'échange entre registres XCHG. Une autre possibilité est une variante de l'instruction STORE en mode d'adressage immédiat. Elle copie une constante dans une adresse mémoire. Elle n'est pas présente sur les architectures ARM, POWER PC ou SPARC. Mais laissons-les de côté. Pour rappel, les instructions d'accès mémoire copient une donnée d'une source vers une destination, les deux ayant leur propre mode d'adressage. Il n'est pas systématiquement encodé. Par exemple, l'instruction LOAD charge une donnée dans un registre destination, l'adressage de la destination est donc toujours l'adressage inhérent (nom de registre). Pas besoin d'encoder le mode d'adressage, juste la référence, le nom de registre. Idem pour les autres instructions. {|class="wikitable" |- ! colspan="7" | Instruction LOAD |- | Opcode | Adressage de la source * Adressage absolu (adresse) * Adressage indirect à registre * Adressage base + indice * etc. | Adresse ou référence de la source | | Nom de registre de destination |- ! colspan="7" | Instruction STORE |- | Opcode | | Nom de registre source | Adressage de la destination * Adressage absolu (adresse) * Adressage indirect à registre * Adressage base + indice * etc. | Adresse ou référence de destination |- ! colspan="7" | Instruction MOV |- | rowspan="2" | Opcode | rowspan="2" | * Adressage inhérent (registre) * Adressage immédiat (constante) | Nom de registre source | rowspan="2" | | rowspan="2" | Nom de registre destination |- | Constante immédiate |} Les processeurs x86, et sans doute quelques autres, fusionnent les instructions LOAD, STORE et MOV en une seule instruction appelée MOV. Elle est capable de faire une lecture, une écriture et une copie entre registres. Par contre, elle n'est pas capable de faire une copie d'une adresse mémoire vers une autre. Elles encodent une source et une destination, dont au moins l'une d'entre elle est un registre. Leur encodage place le registre juste après l'opcode, et la seconde opérande après. Un bit de l'instruction dit si la première opérande est la source ou la destination. {|class="wikitable" |+ Encodage d'une instruction MOV généraliste |- | rowspan="3" | Opcode | rowspan="3" | Bit qui indique l'ordre des champs | rowspan="3" | Nom de registre source/destination | rowspan="3" | * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source/destination |- | Adresse mémoire ou registres pour les pointeurs |- | Constante immédiate |} ===Les instructions dyadiques simple-accès et leur encodage=== Les instructions arithmétiques et logique sont elles aussi concernées par la contrainte d'un accès mémoire par instruction. Pour ces opérations, il y a deux possibilités : lire un opérande en RAM, enregistrer le résultat en RAM. Les deux options peuvent s'émuler avec deux instructions consécutives. La première option remplace une instruction LOAD suivie d'une instruction arithmétique/logique, la seconde remplace une instruction arithmétique/logique suivie d'une instruction STORE. Dans les deux cas, on économise un registre : le registre pour charger l'opérande dans le premier cas, le registre pour le résultat dans le second cas. Dans les faits, la seconde option n'est jamais appliquée, le résultat est systématiquement stocké dans un registre. En effet, il est fréquent qu'un opérande soit chargé depuis la mémoire, alors qu'il est rare qu'un résultat soit écrit en mémoire après avoir été calculé. Un résultat calculé a de fortes chances d'être réutilisé par la suite, donc de servir d'opérandes sous peu. Mieux vaut le laisser dans les registres, plutôt que de payer le prix d'un accès mémoire. Lire un opérande en mémoire RAM est par contre beaucoup plus fréquent et donc intéressant. [[File:Isregmem.png|vignette|Instruction dyadique de type ''load-op''.]] Dans ce qui suit, nous parlerons d''''instructions ''load-op''''' pour désigner de telles instructions. Elles lisent un opérande en RAM, mais l'autre opérande est lu depuis les registres et le résultat est enregistré dans les registres. Elles sont opposées aux '''instructions reg-reg''', ce qui est une abréviation pour registre-registre. Ces dernières lisent leurs deux opérandes dans les registres, idem pour l'écriture du résultat. Il faut aussi citer les '''instructions cst-reg''', où un opérande est fourni via adressage immédiat, l'autre opérande est dans les registres. Avec la contrainte d'un seul accès RAM par instruction, ce sont les seuls types d'instruction qui existent. {|class="wikitable" |+ Encodage d'une instruction dyadique reg-reg |- ! rowspan="2" | Opcode | rowspan="2" | * Adressage inhérent (registre) * Adressage immédiat (constante) | Nom de registre pour l'opérande 1 | rowspan="2" | Nom de registre pour l'opérande 2 | rowspan="2" | Nom de registre pour le résultat |- | Constante immédiate |} {|class="wikitable" |+ Encodage d'une instruction ''load-op'' |- ! rowspan="3" | Opcode | rowspan="3" | * Adressage inhérent (registre) * Adressage immédiat (constante) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source | rowspan="3" | | rowspan="3" | Nom de registre destination |- | Constante immédiate |- | Adresse mémoire ou registres pour les pointeurs |} Encoder une instruction ''load-op'' est compliqué. En soit, encoder l'opcode et les numéros de registre ne prend pas beaucoup de place, mais encoder une adresse complète prend beaucoup de bits. Une première solution utilise des instructions de taille variable : les instructions reg-reg sont plus courtes que les instructions ''load-op'', ces dernières prenant plus de place pour coder une adresse. Mais gérer des instructions de taille variable est complexe. Aussi, il existe une solution qui marche avec des instructions de taille fixe, à savoir si toutes les instructions sont codées sur le même nombre d'octets (souvent 2, 4 ou 8 octets selon le processeur) : utiliser l'encodage 2-opérandes. ==Les instructions multi-accès et leur encodage== Passons maintenant aux instructions multi-accès. Nous allons séparer les instructions d'accès mémoire d'un côté, et les instructions dyadiques de l'autre. ===Les instructions mémoire-mémoire=== Les instructions que nous allons voir dans ce qui suit sont des instructions d'accès mémoire, qui sont en plus multi-accès. Elles sont connues sous le nom d''''instruction mémoire-mémoire''', ce qui signifie qu'elle lisent une donnée en mémoire, pour l'enregistrer en mémoire. Les instructions mémoire-mémoire les plus communes sont les '''instructions de copie mémoire''', où une adresse est copiée dans une autre. Les copies en mémoire sont des opérations très fréquentes, il est très fréquent qu'un programme copie un bloc de mémoire dans un autre et beaucoup de programmeurs ont déjà été confronté à un tel cas. Aussi, les processeurs ajoutent des instructions multi-accès pour accélérer ces copies, ce qui fait un bon compromis entre performance et simplicité d'implémentation. Avec elles, la source et la destination sont systématiquement des adresses, mais, il faut préciser leur mode d'adressage : absolu, pointeur, autre. Pour préciser le mode d'adressage, il est possible de préciser un mode d’adressage différent pour la source et de la destination. Par exemple, on peut utiliser le mode d'adressage base + indice pour la source, l'adressage absolu pour la destination. {|class="wikitable" |+ Encodage d'une instruction de copie mémoire |- ! Opcode | Mode d'adressage de la source * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice * etc. | Mode d'adressage de la destination * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice * etc. | Référence(s) vers la source | Référence(s) vers la destination |} Une autre solution utilise le même mode d'adressage pour la source et la destination. On perd en flexibilité, mais ce n'est pas si grave. Les instructions de copie mémoire sont souvent utilisées pour copier des tableaux, qui sont souvent adressés avec l'adressage base + indice ou indirect à registre. Par contre, l'encodage de l'instruction est plus simple, on économise quelques bits. {|class="wikitable" |+ Encodage d'une instruction de copie mémoire |- ! Opcode | Mode d'adressage de la source et de la destination * Adressage absolu (adresse) * Adressages mémoire indirect à registre (pointeur) * Adressages mémoire base + indice | Référence(s) vers la source | Référence(s) vers la destination |} Les instructions multi-accès précédentes demandent d'encoder deux adresses dans l'instruction, ainsi que le mode d'adressage pour ces deux adresses. Les instructions sont donc assez longues et ne tiennent pas dans une instruction de taille fixe, des instructions de taille variables sont utilisées. Quelques processeurs fusionnent les instructions de copie mémoire avec les instructions LOAD, STORE et MOV. Elles ont alors une '''instruction mémoire généraliste''', souvent appelée MOV. Elle est capable de faire une lecture, une écriture, une copie entre registres et une copie d'une adresse mémoire vers une autre. Source comme destination doivent encoder le mode d'adressage adéquat, elles n'utilisent pas le même mode d'adressage. Typiquement, le premier opérande désigne la source et l'autre la destination. * Si les deux opérandes sont des registres, le premier registre est copié dans le second. * Si le premier opérande est un registre et l'autre une adresse, c'est une écriture. * Si le premier opérande est une adresse et l'autre un registre, c'est une lecture. * Si les deux opérandes sont des adresses, c'est une copie mémoire. {|class="wikitable" |+ Encodage d'une instruction mémoire généraliste |- ! rowspan="3" | Opcode | rowspan="3" | Mode d'adressage de la source * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) * Adressage immédiat (constante) | rowspan="3" | Mode d'adressage de la destination * Adressage inhérent (registre) * Adressages mémoire (adresse, pointeur, autre) | Nom de registre source | Nom de registre destination |- | Adresse mémoire ou registres pour les pointeurs | Adresse mémoire ou registres pour les pointeurs |- | Constante immédiate |} ===Les instructions multi-accès monadiques et dyadiques=== Il existe de rares instructions multi-accès monadiques. Un exemple est celui des instructions de décalage sur les CPU x86. L'opérande à décaler est lue soit depuis les registres, soit depuis la mémoire, elle est écrite au même endroit. Elle fait donc deux accès mémoire, si l'opérande est en RAM : un pour lire d'opérande, l'autre pour écrire le résultat. L'instruction fournit une seule adresse ou un seul numéro de registre, qui sert à la fois pour la source et pour la destination, pour l'opérande et le résultat. Il s'agit donc d'un encodage 1-référence, une sorte d'équivalent de l'encodage 2-référence pour les instructions monadiques. Les instructions monadiques de ce type, qui lisent une opérande en RAM et écrivent un résultat en RAM, au même endroit, porte un nom spécifique. Elles sont mine de rien assez courante. Tous les processeurs modernes en supportent quelques unes. Mais nous ne pouvons pas en parler à ce moment là du cours. La raison est que ces instructions sont toutes des instructions utilisées sur les processeurs multicœurs, dans des contextes très spécifiques, que nous n'avons pas encore abordé. Pour rentrer dans le détail, il s'agit d'une sous-classe de ces fameuses instructions atomiques appelées '''instructions ''read-modify-write'''''. Tout cela deviendra plus clair une fois qu'on en sera au chapitre sur les instructions atomiques. Les processeurs peuvent gérer quelques instructions multi-accès dyadiques. De rares instructions lisent deux opérandes en mémoire, mais le résultat est enregistré dans les registres. Par exemple, sur les processeurs x86, l'instruction de comparaison CMPS peut lire deux opérandes en mémoire RAM et les comparer. Le résultat de la comparaison est mémorisé dans le registre d'état, pas ailleurs, qui est adressé implicitement. L'instruction lit ses opérandes dans la mémoire, pas ailleurs. Enfin, il existe de rares processeurs où les opérandes peuvent être lus depuis les registres ou la RAM, idem pour l'écriture du résultat qui peut se faire dans les registres ou en RAM. De tels processeurs ont tous des instructions de taille variable. En effet, les instructions vont d'instructions qui encodent deux/trois registres, à des instructions encodant deux/trois adresses. Autant les premières sont très courtes, autant les autres sont très longues. La seule solution pratique est d'utiliser des instructions de taille variable. ==Le cas des processeurs x86== Après avoir vu la théorie, nous allons étudier l'encodage des instructions des CPU x86, présents dans nos PC. Ils ont un des encodage les plus complexe qui soit. Ils utilisent des instructions de longueur variable, qui font de 1 à 15 octets. Le jeu d'instruction contient aussi un grand nombre d'instructions, qui n'a eu de cesse d'augmenter avec le temps. Mais surtout, les instructions en question sont assez complexes. Il supporte des instructions cst-reg, reg-reg et ''load-op'', mais aussi quelques instructions multi-accès. Nous allons d'abord voir les instructions les plus complexes, histoire de voir un exemple concret d'instructions multi-accès, avant de voir comment sont encodées les instructions en général. ===L'encodage d'une instruction x86=== L'encodage d'une instruction x86 contient au minimum un opcode, qui peut-être complété avec des informations annexes. Premièrement, il peut être précédé d'un préfixe, qui complète l'opcode. Il modifie l'interprétation de l'opcode ou des octets qui suivent. Il existe un paquet de préfixes très différents dont nous ne pouvons pas encore parler ici. Il y a notamment un préfixe spécifique pour les instructions 64 bits. Suivant le préfixe, l'opcode peut passer de 1 à 3 octets. Il peut y avoir au maximum 4 préfixes, chacun codé sur un octet. Ils sont facultatifs. Deuxièmement, l'opcode peut être suivi par un octet qui précise le mode d'adressage. Il est facultatif car certaines instructions utilisent uniquement le mode d'adressage implicite. Il est appelé l'octet ModR/M. Troisièmement, l'octet ModR/M peut être suivi par trois informations, qui sont utilisées pour calculer l'adresse de l'opérande chargée depuis la mémoire. Elles regroupent un octet SIB pour l'adressage base + indice, un ''offset'', pour les adressages base + ''offset'' et/ou une constante immédiate. Les trois peuvent être combinées si le mode d'adressage des deux opérandes le permet, mais cela demande d'avoir une opérande immédiate couplée à une opérande en mode d'adressage base + indice + ''offset''. Ce n'est pas très courant. {|class="wikitable" |- ! Préfixes !! opcode !! Mod R/M !! SIB !! Displacement !! Immediate |- | Préfixes || opcode || Mode d'adressage || registres pour l'adressage base + indice || ''offset'' || Constante immédiate |- | facultatif || obligatoire || facultatif || facultatif || facultatif |- | 0 à 4 octets || 1, 2, 3 octets, souvent un seul || 1 octet || 1 octet || 1, 2 ou 4 octets || 1, 2, 4 octets en 32 bits, peut aller jusqu'à 8 en 64 bits. |} L'octet SIB inclu deux numéros de registres : un pour le registre de base, un autre pour le registre d'indice. Il inclu aussi la taille de l'opérande, qui est utilisée pour multiplier l'indice, avant de l'additionner au registre de base. {|class="wikitable" |+ Octet SIB |- ! Taille de l'opérande !! Base !! Indice |- | Entier || Numéro de registre || Numéro de registre |- | 2 bits || 3 bits || 3 bits |} L'octet Mod R/M est composé de trois champs, appelés MOD, REG et R/M. MOD est codé sur deux bits et précise le mode d'adressage : * 00 pour l'adressage indirect à registre ou l'adressage base + indice. * 01 pour dire qu'un ''offset'' de 1 octet est présent. * 10 pour dire qu'un ''offset'' de 4 octets est présent. * 11 si les deux opérandes sont dans un registre. Encoder des numéros de registres sur 3 bits limite le processeur à 8 registres. Les CPU x86 s'en sont contenté jusqu’à la génération 32 bits. Lors du passage au 64 bits, 8 autres registres ont été ajouté. Pour ajouter des registres, l'octet SIB se voit ajouter deux bits, un par numéro de registre, ce qui fait qu'il est alors codé sur 10 bits. Mais cela n'est le cas que si l'octet d'opcode est précédé par l'octet de préfixe adéquat, le préfixe REX, VEX ou XOP. Pour les opérations dyadiques, REG encode le numéro de registre de la première opérande, R/M encode le numéro de registre pour la seconde opérande. Pour les opérations à une seule opérande, le champ REG est ignoré et le numéro de registre est dans le champ R/M. En clair, l'octet Mod R/M a une interprétation qui dépend de l'opcode. De plus, la destination est encodée via encodage 2-opérande. La destination est soit la première opérande, soit la seconde. Un bit de l'opcode permet de choisir, ce qui donne un peu de flexibilité à l'encodage 2-opérande. {|class="wikitable" |+ Octet SIB |- ! Taille de l'opérande !! Base !! Indice |- | Entier || Numéro de registre || Numéro de registre |- | 2 bits || 3 bits || 3 bits |} ===Les ''string instructions''=== Les instructions dont nous allons parler sont connues sous le nom d'''instructions de chaines de caractère'', bien qu'elles travaillent en réalité plus sur des tableaux ou des zones de mémoire. Les opérations arithmétiques et logiques ne peuvent pas lire deux opérandes en mémoire, la possibilité est donc limitée à quelques instructions d'accès mémoire bien précises. Les instructions de chaine de caractère peuvent se classer en deux types : celles qui ne font qu'un seul accès mémoire, et celles qui en font deux. Les instructions de chaine de caractère multi-accès sont les suivantes : * L'instruction MOVS copie une adresse mémoire dans une autre, c'est la seule instruction de copie mémoire sur ces CPU. * L'instruction de comparaison CMPS peut lire deux opérandes en mémoire RAM et les comparer. Le résultat de la comparaison est mémorisé dans le registre d'état, pas ailleurs. Les autres instructions de chaine de caractère regroupent trois instructions : * LOD charge une donnée dans le registre EAX, l'adresse de cette donnée est dans le registre ESI. * STO copie une donnée en mémoire RAM, la donnée est dans le registre EAX, l'adresse mémoire est dans le registre ESI. * SCA charge une donnée et la compare avec le registre EAX, l'adresse de la donnée est dans le registre ESI. Pour les échanges entre deux adresses mémoire, les processeurs x86 fournissent une instruction dédiée, appelée MOVS. L'adresse de la source est dans le registre ESI, l'adresse de destination est dans le registre EDI. Vous remarquerez que les registres utilisés sont fixés à l'avance, ce qui fait qu'ils sont adressés implicitement. Les deux registres sont incrémentés/décrémentés à chaque exécution de l'instruction, afin de pointer vers le mot mémoire suivant. Fait intéressant, les instructions mémoires peuvent être répétées automatiquement plusieurs fois, en leur ajoutant un préfixe, un octet placé avant l'opcode. Le nombre de répétitions doit être stocké dans le registre ECX, qui est décrémenté à chaque exécution de l'instruction. Une fois que le registre ECX atteint 0, l'instruction n'est plus répétée et on passe à l'instruction suivante. En clair, l'instruction décrémente automatiquement ce registre à chaque exécution et s'arrête quand il atteint zéro. Suivant le préfixe, d'autres conditions peuvent être ajoutées. Elle peut être répétée plusieurs fois avec l'ajout du préfixe REP. L'instruction avec préfixe, REPMOVS, permet donc de copier un bloc de mémoire dans un autre, de copier N adresses consécutives ! Cela permet de parcourir la mémoire dans l'ordre montant (adresses croissantes) ou descendant (adresses décroissantes), suivant que les registres sont incrémentés ou décrémentés. La direction de parcours de la mémoire est spécifiée dans un bit incorporé dans le processeur, qui vaut 0 (décrémentation) ou 1 (incrémentation), et peut être altéré par des instructions dédiées, de manière à être mis à 1 ou 0. D'autres instructions d'accès mémoire peuvent être préfixées avec REP. Par exemple, l'instruction STOS, qui copie le contenu du registre EAX dans une adresse. On peut exécuter cette instruction une fois, ou la répéter plusieurs fois avec le préfixe REP. Là encore, on doit préciser l'adresse à laquelle écrire dans un registre spécifié, puis le nombre de répétitions dans le registre ECX. Là encore, le nombre de répétitions est décrémenté à chaque exécution, alors que l'adresse est incrémentée/décrémentée. L'instruction REPSTOS est très utile pour mettre à zéro une zone de mémoire, ou pour initialiser un tableau avec des données préétablies. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les modes d'adressage | prevText=Les modes d'adressage | next=Les jeux d'instructions | nextText=Les jeux d'instructions }} </noinclude> mlugv0o3xv64wgm6vrj48o1xugnht9i Mathc complexes/Sommaire 0 69360 773857 771206 2026-10-01T13:48:35Z Xhungab 23827 773857 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/Introduction|Introduction]] ---- ---- {{Partie{{{type|}}}| '''Propriétés et Applications'''}} : {| class="wikitable" |+ |- | : ==Le langage C== * [[Mathc complexes/a69| '''Utilitaires''']] : * [[Mathc complexes/h07a| '''Les complexes''']] || ==Utilitaires Octave== * [[Mathc complexes/a202| '''Octave.''']] * . |- | ==Utilitaires Matrices== * [https://youtube.com/playlist?list=PLi6peGpf8EPMnU50SHdNTVMVm0C32g5ZX '''Playlist'''] : * [[Mathc complexes/a30| Créer une matrice]] : * [[Mathc_complexes/Fichiers_h_:_v_a5| Afficher une matrice ]] : * [[Mathc complexes/a181| Matrices dans un fichier]] : * [[Mathc complexes/a176| Matrices pour Octave]] : * [[Mathc_complexes/a14| Copier une matrice ]] : * [[Mathc_complexes/a16| Matrices aléatoires ]] : * [[Mathc_complexes/a15| Tableaux de matrices ]] : * [[Mathc complexes/a177| Vecteurs d'une colonne]] : : ''' Opérations de bases''' : * [[Mathc_complexes/a17| Opérations de bases]] : * [[Mathc complexes/a84| La fonction trace_Z();]] : *. || : ==Matrices spécifiques== * [https://youtube.com/playlist?list=PLHLzA3M4Bc0E&si=duP8-069AJR5_SQ5 '''Playlist'''] : * [[Mathc complexes/Fichiers c : test02d| Matrice '''transposée''']] : * [[Mathc complexes/010| Matrice identité]] : * [[Mathc complexes/e01| Matrices triangulaires]] : * [[Mathc complexes/a140| Matrices commutatives]] : * [[Mathc complexes/a171| Matrices semblables]] : * [[Mathc complexes/01i| Matrices hermitiennes]] : * [[Mathc complexes/01l| Matrices anti-hermitiennes]] : * [[Mathc complexes/01u| Matrices définies positives]] : * [[Mathc complexes/09u| Matrices de Gram]] : * [[Mathc complexes/01w| Matrices définies négatives]] : * [[Mathc_complexes/a128| Série de Taylor]] : * [[Mathc complexes/a277| transformée de Fourier discrète]] : |- | ==Déterminant== * [https://youtube.com/playlist?list=PLUrB2WN1GmgY&si=3XZkUOmMxQxwG7UJ '''Playlist'''] : * [[Mathc complexes/a22| Opérations élémentaires]] * [[Mathc complexes/a23| Fonctions intermédiaires]] : : ''' Propriétés mathématiques ''' : * [[Mathc complexes/a24| Déterminant]] : || ==Applications en math== : * [[Mathc complexes/a288| Matrice Adjointe]] * [[Mathc complexes/a25| L'inverse par le déterminant]] * [[Mathc complexes/a207| Produit en croix]] *. *. *. |- | ==Total Pivoting== * [https://youtube.com/playlist?list=PLPiM5bYMbZEo&si=OyZUE43Eh6IgQbvA '''Playlist'''] * [[Mathc complexes/03x|i_Abr_Ac_bc_mZ();]] * [[Mathc complexes/03y|i_AbR_AbC_AC_mZ();]] ''' Total Pivoting ''' : * [[Mathc_complexes/a27| Gauss-Jordan]] : * [[Mathc complexes/a302| L'inverse]] : : ''' Application dans ℝ ''' : * [[Mathc complexes/053| Analyse d'un '''réseau''']] : * [[Mathc complexes/05a| Analyse d'un circuit '''électrique''']] : : ''' Applications en Géometrie dans ℝ''' : * [[Mathc complexes/069| L'équation d'un '''polynôme''']] : * [[Mathc complexes/06a| L'équation d'un '''conique''']] : * [[Mathc complexes/06m| L'équation d'un '''cercle''']] : || ==Partial Pivoting== * [https://youtube.com/playlist?list=PLdTyEkPFzkWM&si=SzLDMcchoEX1RaQF '''Playlist'''] : * [[Mathc_complexes/a35| Variables libres]] : : '''Application dans ℝ''' : * [[Mathc complexes/05h| Équation chimique]] : : ''' Applications mathématiques ''' : * [[Mathc_complexes/a220| Trouver une base pour ...]] : * [[Mathc_complexes/c233| Matrices de changement de base]] : * [[Mathc complexes/03p| Matrice d'une application linéaire]] : * [[Mathc complexes/a245| Projection sur un sous-espace vectoriel]] *. * . * . * . * . |- | ==Produit scalaire== * [https://youtube.com/playlist?list=PLGjxekZ9QUlg&si=2YtMW7v1oIvqrzYx '''Playlist'''] : * [[Mathc_complexes/a19| Produit scalaire]] : * [[Mathc_complexes/a258| Propriétés ]] : * [[Mathc complexes/08m| Calculer les vecteurs orthogonaux ]] : : ''' Orthogonalisation ''' : * [[Mathc complexes/a330|Matrices orthonormales]] : * [[Mathc_complexes/c244| Bases orthonormales]] : : ''' La QR Décompositions ''' : * [[Mathc complexes/a297| QR décomposition]] ''' Application dans ℝ ''' : * [[Mathc complexes/a326| Analyse d'un '''réseau''']] : * [[Mathc complexes/a64| Analyse d'un circuit '''électrique''']] : : ''' Applications en Géometrie dans ℝ''' : * [[Mathc complexes/a21| L'équation d'un '''polynôme''']] : * [[Mathc complexes/a26| L'équation d'un '''conique''']] : * [[Mathc complexes/a18| L'équation d'un '''cercle''']] * . * . * . * . * . * . * . * . : : || ==Vecteurs propres== : * [https://youtube.com/playlist?list=PLHxmcw9nFNuk&si=SwZ3uqHBesDg2Fd1 '''Playlist'''] : * Matrices symétriques conjuguées : * [[Mathc_complexes/a20| Vecteurs propres ]] : * [[Mathc complexes/a296| Quelques propriétés]] : * [[Mathc complexes/02b| Valeurs propres multiples]] : * '''Non''' symétriques conjuguées * [[Mathc complexes/009| Vecteurs propres.]] : * [[Mathc complexes/a303| Quelques propriétés]] : : '''Étude du code ''' : * [[Mathc complexes/02c| eigs_mZ(A,EigsValue);]] : : ''' Applications en mathématique ''' : * Matrices symétriques conjuguées * [[Mathc_complexes/a115| Fonctions matricielles]] : * [[Mathc complexes/a9c| La décomposition spectral]] : * '''Non''' symétriques conjuguées * [[Mathc complexes/00g| Fonctions matricielles]] : : ''' Applications graphique dans ℝ''' * [[Mathc complexes/09h| Projection du plan(1) ]] * [[Mathc complexes/09i| Projection du plan(2) ]] : * [[Mathc complexes/090| Projection de l'espace(1)]] * [[Mathc complexes/091| Projection de l'espace(2)]] : * [[Mathc complexes/08q| Projection de l'hyperespace(1)]] * [[Mathc complexes/08r| Projection de l'hyperespace(2)]] * . : : |- | ==Décomposition X== * [https://youtube.com/playlist?list=PLcvBw5BCUjI0&si=WO4Euuev1Hubwlmm '''Playlist'''] : * [[Mathc complexes/a329|Plus de lignes que de colonnes]] : * [[Mathc complexes/a1163|Plus de colonnes que de lignes]] : || ==Fonctions matricielles== : * [[Mathc complexes/028|Fonctions matricielles]] *. *. : |- | ==Pseudo inverses== : * [https://youtube.com/playlist?list=PLJJ7QKop2EJw&si=v98w0PMbD9tPpPzH '''Playlist'''] : ''' Pseudo inverse gauche ''' * [[Mathc complexes/046| Pinv = invR Q_T]] : * [[Mathc complexes/047| Pinv = '''inv(A_TA) A_T''']] : * [[Mathc complexes/048| Pinv = V invX '''U_T''']] : : ''' Pseudo inverse droit ''' : * [[Mathc complexes/02y| Pinv = '''A_T inv(AA_T)''']] : * [[Mathc complexes/02x| Pinv = U invX '''V_T''']] *. *. *. *. *. *. || ==Applications dans ℝ== : * Avec : Pinv = '''inv(A_TA) A_T''' : * [[Mathc complexes/084| L'équation d'un '''polynôme''']] : * [[Mathc complexes/085| L'équation d'un '''conique''']] : * [[Mathc complexes/086| L'équation d'un '''cercle''']] : ''' Application dans ℝ ''' : * Avec : Pinv = '''V invX U_T''' : * [[Mathc complexes/07z| Analyse d'un '''réseau''']] : * [[Mathc complexes/080| Analyse d'un circuit '''électrique''']] : : ''' Applications en Géometrie dans ℝ''' : * Avec : Pinv = '''V invX U_T''' : * [[Mathc complexes/081| L'équation d'un '''polynôme''']] : * [[Mathc complexes/082| L'équation d'un '''conique''']] : * [[Mathc complexes/083| L'équation d'un '''cercle''']] : : : |- | ==Matrices dans ℝ== : * [https://youtube.com/playlist?list=PLHyS-XNKeXbY&si=vaHNIiCzUcaQz6fs '''Playlist'''] : * Symétriques dans ℝ * [[Mathc_complexes/a169| '''Fonctions matricielles''']] : || * Non symétriques dans ℝ * [[Mathc complexes/04d| '''Vecteurs propres''']] * . * . * . |} : : : =='''La bibliothèque'''== : {{Partie{{{type|}}}|[[Mathc complexes/a29| '''La bibliothèque.''']]}} ---- {{Lien modifier|Mathc complexes/Sommaire|modifier le sommaire}} {{AutoCat}} k5wgfj40kms3rok8ifttp0p7j3luh52 Électronique/Les semi-conducteurs 0 71006 773872 676304 2026-10-01T16:00:01Z Д.Ильин 53054 773872 wikitext text/x-wiki Les semi-conducteurs sont des solides dont la résistance varie selon la température ou d'autres paramètres physiques. Ce comportement s'explique par le fait que les électrons des atomes deviennent des électrons libres si on leur donne une énergie suffisante. Les électrons arrachés aux atomes deviennent des porteurs de charge, qui peuvent alors conduire le courant. Le courant dans les semi-conducteurs est dominé par les électrons libres créés par "ionisation" du solide, ce qui fait que l'étude de cette "ionisation" est importante. ==La théorie des bandes des solides== [[File:Semiconductor Bands 0K.svg|vignette|Bandes de conduction et de valence.]] La '''théorie des bandes''' est une théorie physique qui décrit le fonctionnement des semi-conducteurs, mais aussi des conducteurs et des isolants. Nous allons la présenter sous une forme vulgarisée et particulièrement simplifiée. Pour résumer simplement, cette théorie dit que les électrons dans un solide peuvent être dans deux états : soit ils sont fortement liés aux atomes, soit ce sont des électrons libres. Si les électrons libres peuvent servir de porteurs de charges, ce n'est pas le cas des électrons liés. Là où les électrons libres peuvent parcourir la totalité du solide, les électrons liés confinés dans les atomes, d'où ils ne peuvent pas sortir. Les électrons liés appartiennent aux atomes (ils orbitent autour du noyau atomique) et participent aux liaisons chimiques entre atomes. La différence entre les deux tient dans leur énergie : l'attraction atomique enlève un peu d'énergie aux électrons, pour des raisons que nous éludons ici. Les électrons de faible énergie sont liés aux atomes et ne peuvent pas servir de porteurs de charges. À l'inverse, les électrons à forte énergie sont des électrons libres, qui ont quitté l’attraction atomique du fait de leur forte énergie. Dans un solide, l'énergie d'un électron se trouve systématiquement dans deux intervalles possibles : un intervalle de faible énergie et un autre où elle est plus forte. Les électrons liés ont une énergie dans l'intervalle de faible énergie, appelé la '''bande de valence'''. À l'inverse, l'intervalle d'énergie des électrons libres est appelé la '''bande de conduction'''. Entre les deux, on peut trouver un intervalle d'énergie dans lequel aucun électron ne peut se trouver : la '''bande interdite'''. [[File:Metal vs insulator.png|vignette|Différence entre conducteur et isolants en fonction de l'énergie de Fermi et des bandes de Valence/conduction.]] On peut déjà comprendre pourquoi certains matériaux sont naturellement conducteurs ou non. Au zéro absolu, les porteurs de charge vont se répartir dans les bandes de conduction et de valence, selon leur énergie. L'énergie maximale que peut avoir un électron au zéro absolu est appelée l''''énergie de Fermi'''. Selon que cette énergie de Fermi se situe dans la bande de conduction ou non, on se trouve face à deux situations différentes. * Premier cas : l'énergie de Fermi se situe dans la bande de conduction. Des électrons se trouvent alors dans la bande de conduction, même au zéro absolu. Ce qui veut dire que le matériau contient naturellement des porteurs de charges pouvant conduire le courant et est donc conducteur. * Deuxième cas : l'énergie de Fermi se situe dans la bande de valence ou interdite. Dans ce cas, tous les électrons seront dans la bande de valence. Tous les électrons du matériau sont donc liés et aucun porteur de charge n'est disponible. Le matériau est alors isolant ou semi-conducteur (on verra pourquoi les semi-conducteurs sont dans ce cas juste après). ===Semi-conducteurs et isolants=== [[File:Semiconductor Bands Hot.svg|vignette|Dé-confinement d'un électron lié, qui devient un électron libre. Équivalent à un passage d'un électron de la bande de valence à celle de conduction.]] Pour comprendre la différence entre semi-conducteurs et isolants, il faut étudier comment des électrons peuvent transiter entre les bandes de valence et de conduction. Il est possible de dé-confiner un électron en lui fournissant assez d'énergie, ce qui lui permet de vaincre l'attraction atomique et de quitter l'atome. Un bon moyen pour cela est d'augmenter la température du solide, ce qui augmente l'énergie cinétique des électrons. Dit autrement, dé-confiner un électron demande juste de lui fournir assez d'énergie pour passer de la bande de valence vers la bande de conduction. L'énergie qu'il faut fournir pour cela, à savoir la différence d'énergie entre bande de valence et de conduction, est appelée le '''gap d'énergie'''. On peut le voir grossièrement comme une énergie d'ionisation pour un électron dans un solide. La différence entre semi-conducteurs et isolants tient dans le gap d'énergie. * Pour les isolants, le gap d'énergie est important : il faut fournir une grande quantité d’énergie pour dé-confiner un électron. À des températures normales, la totalité des électrons sont des électrons liés aux atomes et le solide ne contient aucun porteur de charges. À de plus hautes températures, il est cependant possible de créer quelques électrons libres, mais rien de bien folichon. * Pour les semi-conducteurs, il faut une faible énergie pour dé-confiner un électron. Une petite augmentation de température suffit pour faire passer des électrons de la bande de valence vers la bande de conduction. Dit autrement, une augmentation de température assez faible permet de créer suffisamment d'électrons libres pour conduire le courant. * Pour les métaux, il n'y a pas besoin de fournir de l'énergie pour dé-confiner les électrons. Le métal contient naturellement des électrons libres, même à de faibles températures. Il conduit donc naturellement le courant. Les métaux sont naturellement ionisés, et on peut les voir comme un cristal ionisé qui baigne dans un fluide d'électrons libres. La bande de valence et de conduction se chevauchent. [[File:Isolator-metal-semicond-fr.svg|centre|vignette|upright=2.0|Illustration du gap d'énergie entre conducteurs, semi-conducteurs et isolants.]] ===Les trous quantiques=== [[File:Electron-hole.svg|vignette|Trous quantiques.]] Quand un électron lié devient un électron libre, l'atome initial s'ionise et obtient une charge positive opposée à celle de l'électron. D'après les règles compliquées de la physique quantique, on peut considérer qu'un déficit de charge se comporte comme une particule de charge positive, appelée '''trou'''. Il ne s'agit pas d'une vraie particule, mais on peut parfaitement faire comme si c'était le cas. Donc, toute formation d'un électron libre donne naissance à un trou dans l'atome qu'il quitte. Le processus inverse, à savoir la disparition d'un trou par capture d'un électron libre, est aussi possible. Il est possible qu'un électron libre se lie à un atome "ionisé", faisant disparaitre le trou. Ce processus est appelé la recombinaison. Chose importante, les trous peuvent parfaitement se déplacer dans le solide, en sautant d'atome en atome, de proche en proche. Ils se comportent comme des porteurs de charge qui circulent dans le solide et permettent de conduire le courant. Pour comprendre pourquoi, il faut rappeler qu'un trou est un déficit d'électron dans un atome. Ce déficit d'électron peut être compensé par recombinaison avec un électron libre, mais aussi par un électron lié provenant d'un atome voisin. Dans ce cas, l'atome fournisseur de l’électron, initialement neutre, hérite du déficit de charge. Ainsi, le déficit d'électron se déplace dans le métal en sautant d'atomes en atomes, de proche en proche. Les électrons de valence étant attirés eux aussi par la borne positive, ils vont sauter d'atomes en atomes avec une préférence pour la direction positive. Ainsi, le courant dans les semi-conducteurs provient de deux sources : un courant d'électrons libres, et le déplacement des électrons atomiques d'atomes en atomes. Le courant de proche en proche des électrons se reformule alors comme un courant de trous, qui sont donc des porteurs de charge. ==Les semi-conducteurs intrinsèques et extrinsèques== Les semi-conducteurs se différencient selon leur densité de trous et d'électron, qui peut varier selon divers paramètres. La température est déterminante pour certains semi-conducteurs, alors qu'elle l'est moins pour d'autres. Cela permet de classer les semi-conducteurs en deux types : * les semi-conducteurs intrinsèques ; * les semi-conducteurs extrinsèques. ===Les semi-conducteurs intrinsèques=== Les '''semi-conducteurs intrinsèques''' sont des semi-conducteurs pour lesquels la résistance dépend de la température. Ils sont totalement isolants au zéro absolu, mais leur résistance diminue quand on les chauffe. Dans ces semi-conducteurs, la formation des électrons-libres est liée exclusivement à la température. La formation des paires électrons-trous a lieu quand un électron reçoit suffisamment d'énergie thermique pour s'arracher de son atome. Il va de soi que plus la température est forte, plus cela a de chances d'arriver : le nombre d'électrons libres augmente donc avec la température, ce qui se répercute sur la conductivité. On peut remarquer que, la formation de chaque électron libre laisse un trou dans le solide. Dit autrement, si on note n la concentration en électrons libres et p la concentration en trous, on a : : <math>n = p = n_i</math> On peut aussi reformuler l'équation précédente de la manière suivante, qui a l'avantage d'être aussi valable pour les semi-conducteurs extrinsèques. : <math>{n_i}^2 = n \cdot p</math> [[File:Semiconduttore intrinseco.png|centre|vignette|upright=2.0|Semi-conducteur intrinsèque. L'illustration montre que le nombre de trous et d'électrons y est le même. On voit aussi que les porteurs de charges n'apparaissent qu'au-delà d'une température suffisante.]] Les physiciens ont démontré une formule qui permet de calculer directement la valeur de n (et donc de p). Dans celle-ci, on utilise les notations suivantes : A une constante qui dépend du matériau étudié, T pour la température, K pour la constante de Boltzmann, <math>E_g</math> la largeur de la bande interdite (la bande de Fermi). Ce dernier paramètre est l'énergie minimum qu'il faut pour générer une paire électron-trou. C’est aussi l'énergie nécessaire pour briser une liaison covalente entre deux atomes, ce qui permet de transformer un électron lié en électron libre. : <math>n_i = A \cdot T^{\frac{3}{2}} \cdot e^{- \frac{1}{2} \frac{E_g}{K T}}</math> ===Les semi-conducteurs extrinsèques=== Mais à côté des semi-conducteurs intrinsèques, on trouve les '''semi-conducteurs extrinsèques''', dont la conduction ne dépend pas que de la température. Leur caractéristique principale est qu'ils ont des porteurs de charges disponibles, même au zéro absolu. On distingue les semi-conducteurs qui contiennent des trous au zéro absolu de ceux qui ont des électrons libres. Les premiers sont appelés '''semi-conducteurs P''' et les seconds des '''semi-conducteurs N'''. Les schémas ci-dessous illustrent cette distinction. Les trous sont en blanc, alors que les électrons libres sont en noir. {| |[[File:N-Type Semiconductor Bands.svg|vignette|Semi-conducteur de type N.]] |[[File:P-Type Semiconductor Bands.svg|vignette|Semi-conducteur de type P.]] |} Il faut noter que des paires électrons-trous peuvent se former sous l'effet de la température, dans un semi-conducteur extrinsèque. Le semi-conducteur extrinsèque fonctionne donc comme un semi-conducteur intrinsèque, la différence étant dans le nombre de porteur de charge total. Là où le semi-conducteur intrinsèque n'a aucun porteur de charges au zéro absolu, le semi-conducteur extrinsèque part avec un peu d'avance. Cela fait que les semi-conducteurs extrinsèques ont naturellement une meilleure conductivité, vu qu'ils possèdent plus de porteurs de charges. Le nombre total de porteurs de charges dépend de la température, mais on peut cependant faire une remarque importante. Pour les deux types de semi-conducteurs, il se forme autant d'électrons libres que de trous sous l'effet de la température. Mais les semi-conducteurs extrinsèques possèdent un déséquilibre de charges au zéro absolu : ils vont avoir plus d'électrons que de trous au zéro absolu, ou l'inverse. Et cet excès n'est pas compensé par la formation de paires électrons-trous : ce déséquilibre est permanent. En clair, à la différence des semi-conducteurs intrinsèques, la quantité de trous n'est pas égale à la quantité d'électrons libres pour les semi-conducteurs extrinsèques. La relation entre nombre de porteurs de charges, valable pour les deux types de semi-conducteurs, est la suivante : : <math>n \cdot p = {n_i}^2</math> ====Le dopage de semi-conducteurs==== Les semi-conducteurs extrinsèques sont fabriqués en ajoutant des impuretés à un semi-conducteur intrinsèque. Les impuretés ajoutent des trous ou des électrons libres au matériau de base, i<!-- le semi-conducteur dopé ou extrinsèque reste toujours électriquement neutre, comme le semi-conducteur intrinsèque : seul le nombre de porteurs de charge augmente. -->, ce qui en fait un semi-conducteur extrinsèque. Ce processus d'ajout d'impuretés s'appelle le dopage, d'où le nom de '''semi-conducteur dopés''' qui est parfois donné aux semi-conducteurs extrinsèques. Lors du dopage, les impuretés vont se lier chimiquement aux atomes du semi-conducteur. Cependant, il restera un électron ou un trou en trop suite à ce processus, qui serviront de porteurs de charges. * Pour les semi-conducteurs de type N, les impuretés sont plus riches en électrons que les atomes du solide. Il y a alors un électron en trop après dopage, qui devient un électron libre. * Pour les semi-conducteurs de type P, les impuretés sont plus pauvres en électrons que les atomes du solide. Il manque un électron suite au dopage, ce qui donne un trou. {| |[[File:N type semiconductor.svg|vignette|Dopage de type N.]] |[[File:P type semiconductor.png|vignette|Dopage de type P.]] |} D'ordinaire, le dopage part d'un morceau de silicium et y ajoute des impuretés en Bore ou en Phosphore. Le silicium peut se lier chimiquement avec quatre atomes via ce qu'on appelle une liaison covalente. Avec celle-ci, l'atome de Silicium partage un électron avec un atome voisin : l'électron appartient alors aux deux atomes.Vu que le silicium peut participer à quatre liaisons covalentes, il peut fournir quatre électrons en tout. Les impuretés vont aussi former des liaisons covalentes avec le silicium, sauf qu'elles peuvent partager 3 ou 5 électrons dans des liaisons covalentes. * Par exemple, le Phosphore partage 5 électrons, et peut former 5 liaisons covalentes. Un atome de Phosphore prendra la place d'un atome de Silicium dans le solide et ne tissera que quatre liaisons covalentes (il ne peut pas en faire plus pour des raisons assez compliquées). Quatre électrons seront donc utilisés dans ces liaisons, ce qui laisse un électron en trop, qui quitte l'atome et devient un électron libre. Le dopage obtenu est dit de type N, à savoir qu'il ajoute des électrons libres dans le Silicium. * Le bore est dans la situation inverse, à savoir qu'il peut partager 3 électrons, ce qui donne trois liaisons covalentes. Il tisse bien quatre liaisons covalentes, le Silicium lui fournissant les électrons manquant, mais il manque quand même un électron. Ce manque n'est autre qu'un trou. Le dopage obtenu est dit de type P, à savoir qu'il ajoute des trous dans le Silicium. {| |- |[[File:Silicon covalent bond.PNG|vignette|Silicum normal, sans impuretés.]] |[[File:N-doped Si.svg|vignette|Silicium dopé avec du Phosphore - dopage de type N.]] |[[File:P-doped Si.svg|vignette|Silicium dopé avec du Bore - dopage de type P.]] |- | |[[File:Schema - n-dotiertes Silicium.svg|vignette|Silicium dopé avec du Phosphore - dopage de type N.]] |[[File:Schema - p-dotiertes Silicium.svg|vignette|Silicium dopé avec du Bore - dopage de type P.]] |} ====Effet du dopage sur les bandes de conduction et de valence==== Le dopage a un effet sur les bandes de valence et de conduction, en augmentant ou diminuant leurs extrémités. Le dopage de type N abaisse la bande de conduction, alors celui de type P remonte la bande de valence. Dans les deux cas, la bande interdite se réduit, ce qui facilite le dé-confinement des électrons. {| |- |[[File:Niveau donneur.svg|vignette|upright=1.5|Effet du dopage de type N sur les bandes de conduction et de valence. Seules sont représentées la limite basse de la bande de conduction et la limite haute de la bande de valence.]] |[[File:Niveau accepteur.svg|vignette|upright=1.5|Effet du dopage de type P sur les bandes de conduction et de valence. Seules sont représentées la limite basse de la bande de conduction et la limite haute de la bande de valence.]] |} ==La conductivité d'un semi-conducteur== Les parties précédentes ont surtout abordé la quantité de trous et d'électrons présente dans un semi-conducteur. Si nous avons passé beaucoup de temps à étudier ce sujet, c'est parce que celle-ci explique pourquoi la conductivité des semi-conducteurs varie. Dans cette section, nous allons voir qu'il existe un lien très fort entre la résistance d'un matériau et le nombre de porteurs de charges. Plus il y a de porteurs de charges (électrons libres ou trous), plus le matériau pourra conduire facilement le courant. Et donc, tout ce qui peut faire varier le nombre de porteur de charge a une influence directe sur la résistance du matériau. Prenons par exemple un semi-conducteur intrinsèque : une augmentation de la température va augmenter la quantité d'électrons et de trous, ce qui va diminuer la résistance du semi-conducteur. Voyons ce mécanisme plus en détails. Les semi-conducteurs peuvent conduire le courant grâce à deux mécanismes distincts. Le premier est à l'origine de la résistance du matériau et se décrit assez bien avec la loi d'Ohm. Il donne naissance à un courant appelé courant de dérive. Le second permet d'homogénéiser la densité de charge dans le matériau, celles-ci tendant à se répartir uniformément dans l'espace. Cela induit un déplacement de charge appelé courant de diffusion. Le courant dans un semi-conducteur est la somme d'un courant de dérive <math>I_{derive}</math> et du courant de diffusion <math>I_{diffusion}</math>. : <math>I_{total} = I_{derive} + I_{diffusion}</math> ===Le courant de dérive=== Le premier, appelé le '''courant de dérive''', est aussi présent chez les conducteurs normaux (comprendre les métaux et électrolytes). Il apparait quand on soumet le semi-conducteur à une tension/un champ électrique et sa valeur dépend de cette première ainsi que de la résistance du matériau. C'est lui qui intervient dans la loi d'Ohm pour les conducteurs. La conductivité d'un matériau, définie pour le courant de dérive, dépend de la densité de porteurs de charges <math>n</math> et de leur mobilité <math>\mu</math>. : <math>\sigma = n \cdot q \cdot \mu</math> Cette équation nous dit que plus le nombre de porteurs de charges d'un matériau est grand, plus sa conductivité sera importante. On retrouve donc ce qu'on a dit plus haut : les conducteurs le sont parce qu'ils ont un grand nombre de porteurs de charges, alors que les isolants le sont parce qu'ils n'en ont pas. Pour les semi-conducteurs, la situation est intermédiaire : le nombre de porteurs de charges est très faible en temps normal, mais augmente fortement avec la température, l’éclairement, ou d'autres paramètres physiques. Pour les semi-conducteurs, cette formule s'applique partiellement (il faut rajouter un second courant dit de diffusion qui provient de la concentration des porteurs de charges). Quoiqu’il en soit, on a un courant de trous et un autre courant d'électrons. Ces deux types de charges ont certes la même charge, mais leur densité n'est pas la même, pas plus que leurs mobilités <math>\mu_e</math> et <math>\mu_p</math>. En faisant la somme des deux courants, on a : : <math>\sigma = ( n_e \cdot q \cdot \mu_e ) + ( n_p \cdot q \cdot \mu_p ) </math> : <math>\sigma = q \cdot ( n_e \cdot \mu_e + n_p \cdot \mu_p ) </math> ===Le courant de diffusion=== Outre le courant de dérive vu dans le section précédente, les semi-conducteurs sont parcourus par un second type de courant. Celui-ci, le '''courant de diffusion''', vient d'une différence de concentration des porteurs de charges. Ceux-ci ne sont pas forcément répartis de manière homogène dans le matériau, du fait des hasards de la formation des paires électrons-trous. Il arrive donc que les électrons soient plus nombreux à un endroit du matériau que dans d'autres. Dit autrement, la concentration en électrons libres et/ou en trous n'est pas uniforme et varie dans le matériau. Mais ces différences de concentration se traduisent par une répartition inhomogène des charges : le matériau sera chargé négativement là où il y a un excès d'électrons, positivement là où il en manque, en suivant le gradient de concentration. Ce mouvement de charges donne naissance à un courant dit de diffusion, régit par les lois physiques de la diffusion et décrit par l'équation de la diffusion : * j est la densité de courant en un point du matériau ; * e est la charge électrique d'un électron/trou ; * D est un coefficient appelé coefficient de diffusion ; * <math>\frac{d \rho}{dx}</math> est le gradient de concentration des charges. Voici l'équation pour le courant d'électrons libres : : <math>j = e \cdot D \cdot \frac{d \rho}{dx}</math> Et voici l'équation pour les trous : : <math>j = - e \cdot D \cdot \frac{d \rho}{dx}</math> Il existe une relation entre le coefficient de diffusion et la mobilité <math>\mu</math> des porteurs de charge. Celle-ci est appelée la relation d'Einstein : : <math>D = \mu \cdot \frac{K T}{e} = \mu \cdot V_T</math> Le terme : <math>V_T = \frac{K T}{e}</math> est appelé la '''tension thermique'''. <noinclude> {{NavChapitre | book=Électronique | prev=Les oscillateurs (générateurs de fréquence) | prevText=Les oscillateurs (générateurs de fréquence) | next=Les jonctions PN | nextText=Les jonctions PN }}{{autocat}} </noinclude> pdpcovqje2xyaqyshvmupc1luhe6v4q 773874 773872 2026-10-01T16:53:49Z Д.Ильин 53054 773874 wikitext text/x-wiki Les semi-conducteurs sont des solides dont la résistance varie selon la température ou d'autres paramètres physiques. Ce comportement s'explique par le fait que les électrons des atomes deviennent des électrons libres si on leur donne une énergie suffisante. Les électrons arrachés aux atomes deviennent des porteurs de charge, qui peuvent alors conduire le courant. Le courant dans les semi-conducteurs est dominé par les électrons libres créés par "ionisation" du solide, ce qui fait que l'étude de cette "ionisation" est importante. ==La théorie des bandes des solides== [[File:Semiconductor Bands 0K.svg|vignette|Bandes de conduction et de valence.]] La '''théorie des bandes''' est une théorie physique qui décrit le fonctionnement des semi-conducteurs, mais aussi des conducteurs et des isolants. Nous allons la présenter sous une forme vulgarisée et particulièrement simplifiée. Pour résumer simplement, cette théorie dit que les électrons dans un solide peuvent être dans deux états : soit ils sont fortement liés aux atomes, soit ce sont des électrons libres. Si les électrons libres peuvent servir de porteurs de charges, ce n'est pas le cas des électrons liés. Là où les électrons libres peuvent parcourir la totalité du solide, les électrons liés confinés dans les atomes, d'où ils ne peuvent pas sortir. Les électrons liés appartiennent aux atomes (ils orbitent autour du noyau atomique) et participent aux liaisons chimiques entre atomes. La différence entre les deux tient dans leur énergie : l'attraction atomique enlève un peu d'énergie aux électrons, pour des raisons que nous éludons ici. Les électrons de faible énergie sont liés aux atomes et ne peuvent pas servir de porteurs de charges. À l'inverse, les électrons à forte énergie sont des électrons libres, qui ont quitté l’attraction atomique du fait de leur forte énergie. Dans un solide, l'énergie d'un électron se trouve systématiquement dans deux intervalles possibles : un intervalle de faible énergie et un autre où elle est plus forte. Les électrons liés ont une énergie dans l'intervalle de faible énergie, appelé la '''bande de valence'''. À l'inverse, l'intervalle d'énergie des électrons libres est appelé la '''bande de conduction'''. Entre les deux, on peut trouver un intervalle d'énergie dans lequel aucun électron ne peut se trouver : la '''bande interdite'''. [[File:Metal vs insulator.png|vignette|Différence entre conducteur et isolants en fonction de l'énergie de Fermi et des bandes de Valence/conduction.]] On peut déjà comprendre pourquoi certains matériaux sont naturellement conducteurs ou non. Au zéro absolu, les porteurs de charge vont se répartir dans les bandes de conduction et de valence, selon leur énergie. L'énergie maximale que peut avoir un électron au zéro absolu est appelée l''''énergie de Fermi'''. Selon que cette énergie de Fermi se situe dans la bande de conduction ou non, on se trouve face à deux situations différentes. * Premier cas : l'énergie de Fermi se situe dans la bande de conduction. Des électrons se trouvent alors dans la bande de conduction, même au zéro absolu. Ce qui veut dire que le matériau contient naturellement des porteurs de charges pouvant conduire le courant et est donc conducteur. * Deuxième cas : l'énergie de Fermi se situe dans la bande de valence ou interdite. Dans ce cas, tous les électrons seront dans la bande de valence. Tous les électrons du matériau sont donc liés et aucun porteur de charge n'est disponible. Le matériau est alors isolant ou semi-conducteur (on verra pourquoi les semi-conducteurs sont dans ce cas juste après). ===Semi-conducteurs et isolants=== [[File:Semiconductor Bands Hot.svg|vignette|Dé-confinement d'un électron lié, qui devient un électron libre. Équivalent à un passage d'un électron de la bande de valence à celle de conduction.]] Pour comprendre la différence entre semi-conducteurs et isolants, il faut étudier comment des électrons peuvent transiter entre les bandes de valence et de conduction. Il est possible de dé-confiner un électron en lui fournissant assez d'énergie, ce qui lui permet de vaincre l'attraction atomique et de quitter l'atome. Un bon moyen pour cela est d'augmenter la température du solide, ce qui augmente l'énergie cinétique des électrons. Dit autrement, dé-confiner un électron demande juste de lui fournir assez d'énergie pour passer de la bande de valence vers la bande de conduction. L'énergie qu'il faut fournir pour cela, à savoir la différence d'énergie entre bande de valence et de conduction, est appelée le '''gap d'énergie'''. On peut le voir grossièrement comme une énergie d'ionisation pour un électron dans un solide. La différence entre semi-conducteurs et isolants tient dans le gap d'énergie. * Pour les isolants, le gap d'énergie est important : il faut fournir une grande quantité d’énergie pour dé-confiner un électron. À des températures normales, la totalité des électrons sont des électrons liés aux atomes et le solide ne contient aucun porteur de charges. À de plus hautes températures, il est cependant possible de créer quelques électrons libres, mais rien de bien folichon. * Pour les semi-conducteurs, il faut une faible énergie pour dé-confiner un électron. Une petite augmentation de température suffit pour faire passer des électrons de la bande de valence vers la bande de conduction. Dit autrement, une augmentation de température assez faible permet de créer suffisamment d'électrons libres pour conduire le courant. * Pour les métaux, il n'y a pas besoin de fournir de l'énergie pour dé-confiner les électrons. Le métal contient naturellement des électrons libres, même à de faibles températures. Il conduit donc naturellement le courant. Les métaux sont naturellement ionisés, et on peut les voir comme un cristal ionisé qui baigne dans un fluide d'électrons libres. La bande de valence et de conduction se chevauchent. [[File:Isolator-metal-semicond-fr.svg|centre|vignette|upright=2.0|Illustration du gap d'énergie entre conducteurs, semi-conducteurs et isolants.]] ===Les trous quantiques=== [[File:Electron-hole.svg|vignette|Trous quantiques.]] Quand un électron lié devient un électron libre, l'atome initial s'ionise et obtient une charge positive opposée à celle de l'électron. D'après les règles compliquées de la physique quantique, on peut considérer qu'un déficit de charge se comporte comme une particule de charge positive, appelée '''trou'''. Il ne s'agit pas d'une vraie particule, mais on peut parfaitement faire comme si c'était le cas. Donc, toute formation d'un électron libre donne naissance à un trou dans l'atome qu'il quitte. Le processus inverse, à savoir la disparition d'un trou par capture d'un électron libre, est aussi possible. Il est possible qu'un électron libre se lie à un atome "ionisé", faisant disparaitre le trou. Ce processus est appelé la recombinaison. Chose importante, les trous peuvent parfaitement se déplacer dans le solide, en sautant d'atome en atome, de proche en proche. Ils se comportent comme des porteurs de charge qui circulent dans le solide et permettent de conduire le courant. Pour comprendre pourquoi, il faut rappeler qu'un trou est un déficit d'électron dans un atome. Ce déficit d'électron peut être compensé par recombinaison avec un électron libre, mais aussi par un électron lié provenant d'un atome voisin. Dans ce cas, l'atome fournisseur de l’électron, initialement neutre, hérite du déficit de charge. Ainsi, le déficit d'électron se déplace dans le métal en sautant d'atomes en atomes, de proche en proche. Les électrons de valence étant attirés eux aussi par la borne positive, ils vont sauter d'atomes en atomes avec une préférence pour la direction positive. Ainsi, le courant dans les semi-conducteurs provient de deux sources : un courant d'électrons libres, et le déplacement des électrons atomiques d'atomes en atomes. Le courant de proche en proche des électrons se reformule alors comme un courant de trous, qui sont donc des porteurs de charge. ==Les semi-conducteurs intrinsèques et extrinsèques== Les semi-conducteurs se différencient selon leur densité de trous et d'électron, qui peut varier selon divers paramètres. La température est déterminante pour certains semi-conducteurs, alors qu'elle l'est moins pour d'autres. Cela permet de classer les semi-conducteurs en deux types : * les semi-conducteurs intrinsèques ; * les semi-conducteurs extrinsèques. ===Les semi-conducteurs intrinsèques=== Les '''semi-conducteurs intrinsèques''' sont des semi-conducteurs pour lesquels la résistance dépend de la température. Ils sont totalement isolants au zéro absolu, mais leur résistance diminue quand on les chauffe. Dans ces semi-conducteurs, la formation des électrons-libres est liée exclusivement à la température. La formation des paires électrons-trous a lieu quand un électron reçoit suffisamment d'énergie thermique pour s'arracher de son atome. Il va de soi que plus la température est forte, plus cela a de chances d'arriver : le nombre d'électrons libres augmente donc avec la température, ce qui se répercute sur la conductivité. On peut remarquer que, la formation de chaque électron libre laisse un trou dans le solide. Dit autrement, si on note n la concentration en électrons libres et p la concentration en trous, on a : : <math>n = p = n_i</math> On peut aussi reformuler l'équation précédente de la manière suivante, qui a l'avantage d'être aussi valable pour les semi-conducteurs extrinsèques. : <math>{n_i}^2 = n \cdot p</math> [[File:Semiconduttore intrinseco.png|centre|vignette|upright=2.0|Semi-conducteur intrinsèque. L'illustration montre que le nombre de trous et d'électrons y est le même. On voit aussi que les porteurs de charges n'apparaissent qu'au-delà d'une température suffisante.]] Les physiciens ont démontré une formule qui permet de calculer directement la valeur de n (et donc de p). Dans celle-ci, on utilise les notations suivantes : A une constante qui dépend du matériau étudié, T pour la température, K pour la constante de Boltzmann, <math>E_g</math> la largeur de la bande interdite (la bande de Fermi). Ce dernier paramètre est l'énergie minimum qu'il faut pour générer une paire électron-trou. C’est aussi l'énergie nécessaire pour briser une liaison covalente entre deux atomes, ce qui permet de transformer un électron lié en électron libre. : <math>n_i = A \cdot T^{\frac{3}{2}} \cdot e^{- \frac{1}{2} \frac{E_g}{K T}}</math> ===Les semi-conducteurs extrinsèques=== Mais à côté des semi-conducteurs intrinsèques, on trouve les '''semi-conducteurs extrinsèques''', dont la conduction ne dépend pas que de la température. Leur caractéristique principale est qu'ils ont des porteurs de charges disponibles, même au zéro absolu. On distingue les semi-conducteurs qui contiennent des trous au zéro absolu de ceux qui ont des électrons libres. Les premiers sont appelés '''semi-conducteurs P''' et les seconds des '''semi-conducteurs N'''. Les schémas ci-dessous illustrent cette distinction. Les trous sont en blanc, alors que les électrons libres sont en noir. {| |[[File:N-Type Semiconductor Bands.svg|vignette|Semi-conducteur de type N.]] |[[File:P-Type Semiconductor Bands.svg|vignette|Semi-conducteur de type P.]] |} Il faut noter que des paires électrons-trous peuvent se former sous l'effet de la température, dans un semi-conducteur extrinsèque. Le semi-conducteur extrinsèque fonctionne donc comme un semi-conducteur intrinsèque, la différence étant dans le nombre de porteur de charge total. Là où le semi-conducteur intrinsèque n'a aucun porteur de charges au zéro absolu, le semi-conducteur extrinsèque part avec un peu d'avance. Cela fait que les semi-conducteurs extrinsèques ont naturellement une meilleure conductivité, vu qu'ils possèdent plus de porteurs de charges. Le nombre total de porteurs de charges dépend de la température, mais on peut cependant faire une remarque importante. Pour les deux types de semi-conducteurs, il se forme autant d'électrons libres que de trous sous l'effet de la température. Mais les semi-conducteurs extrinsèques possèdent un déséquilibre de charges au zéro absolu : ils vont avoir plus d'électrons que de trous au zéro absolu, ou l'inverse. Et cet excès n'est pas compensé par la formation de paires électrons-trous : ce déséquilibre est permanent. En clair, à la différence des semi-conducteurs intrinsèques, la quantité de trous n'est pas égale à la quantité d'électrons libres pour les semi-conducteurs extrinsèques. La relation entre nombre de porteurs de charges, valable pour les deux types de semi-conducteurs, est la suivante : : <math>n \cdot p = {n_i}^2</math> ====Le dopage de semi-conducteurs==== Les semi-conducteurs extrinsèques sont fabriqués en ajoutant des impuretés à un semi-conducteur intrinsèque. Les impuretés ajoutent des trous ou des électrons libres au matériau de base, i<!-- le semi-conducteur dopé ou extrinsèque reste toujours électriquement neutre, comme le semi-conducteur intrinsèque : seul le nombre de porteurs de charge augmente. -->, ce qui en fait un semi-conducteur extrinsèque. Ce processus d'ajout d'impuretés s'appelle le dopage, d'où le nom de '''semi-conducteur dopés''' qui est parfois donné aux semi-conducteurs extrinsèques. Lors du dopage, les impuretés vont se lier chimiquement aux atomes du semi-conducteur. Cependant, il restera un électron ou un trou en trop suite à ce processus, qui serviront de porteurs de charges. * Pour les semi-conducteurs de type N, les impuretés sont plus riches en électrons que les atomes du solide. Il y a alors un électron en trop après dopage, qui devient un électron libre. * Pour les semi-conducteurs de type P, les impuretés sont plus pauvres en électrons que les atomes du solide. Il manque un électron suite au dopage, ce qui donne un trou. {| |[[File:N type semiconductor.svg|vignette|Dopage de type N.]] |[[File:P type semiconductor.svg|vignette|Dopage de type P.]] |} D'ordinaire, le dopage part d'un morceau de silicium et y ajoute des impuretés en Bore ou en Phosphore. Le silicium peut se lier chimiquement avec quatre atomes via ce qu'on appelle une liaison covalente. Avec celle-ci, l'atome de Silicium partage un électron avec un atome voisin : l'électron appartient alors aux deux atomes.Vu que le silicium peut participer à quatre liaisons covalentes, il peut fournir quatre électrons en tout. Les impuretés vont aussi former des liaisons covalentes avec le silicium, sauf qu'elles peuvent partager 3 ou 5 électrons dans des liaisons covalentes. * Par exemple, le Phosphore partage 5 électrons, et peut former 5 liaisons covalentes. Un atome de Phosphore prendra la place d'un atome de Silicium dans le solide et ne tissera que quatre liaisons covalentes (il ne peut pas en faire plus pour des raisons assez compliquées). Quatre électrons seront donc utilisés dans ces liaisons, ce qui laisse un électron en trop, qui quitte l'atome et devient un électron libre. Le dopage obtenu est dit de type N, à savoir qu'il ajoute des électrons libres dans le Silicium. * Le bore est dans la situation inverse, à savoir qu'il peut partager 3 électrons, ce qui donne trois liaisons covalentes. Il tisse bien quatre liaisons covalentes, le Silicium lui fournissant les électrons manquant, mais il manque quand même un électron. Ce manque n'est autre qu'un trou. Le dopage obtenu est dit de type P, à savoir qu'il ajoute des trous dans le Silicium. {| |- |[[File:Silicon covalent bond.PNG|vignette|Silicum normal, sans impuretés.]] |[[File:N-doped Si.svg|vignette|Silicium dopé avec du Phosphore - dopage de type N.]] |[[File:P-doped Si.svg|vignette|Silicium dopé avec du Bore - dopage de type P.]] |- | |[[File:Schema - n-dotiertes Silicium.svg|vignette|Silicium dopé avec du Phosphore - dopage de type N.]] |[[File:Schema - p-dotiertes Silicium.svg|vignette|Silicium dopé avec du Bore - dopage de type P.]] |} ====Effet du dopage sur les bandes de conduction et de valence==== Le dopage a un effet sur les bandes de valence et de conduction, en augmentant ou diminuant leurs extrémités. Le dopage de type N abaisse la bande de conduction, alors celui de type P remonte la bande de valence. Dans les deux cas, la bande interdite se réduit, ce qui facilite le dé-confinement des électrons. {| |- |[[File:Niveau donneur.svg|vignette|upright=1.5|Effet du dopage de type N sur les bandes de conduction et de valence. Seules sont représentées la limite basse de la bande de conduction et la limite haute de la bande de valence.]] |[[File:Niveau accepteur.svg|vignette|upright=1.5|Effet du dopage de type P sur les bandes de conduction et de valence. Seules sont représentées la limite basse de la bande de conduction et la limite haute de la bande de valence.]] |} ==La conductivité d'un semi-conducteur== Les parties précédentes ont surtout abordé la quantité de trous et d'électrons présente dans un semi-conducteur. Si nous avons passé beaucoup de temps à étudier ce sujet, c'est parce que celle-ci explique pourquoi la conductivité des semi-conducteurs varie. Dans cette section, nous allons voir qu'il existe un lien très fort entre la résistance d'un matériau et le nombre de porteurs de charges. Plus il y a de porteurs de charges (électrons libres ou trous), plus le matériau pourra conduire facilement le courant. Et donc, tout ce qui peut faire varier le nombre de porteur de charge a une influence directe sur la résistance du matériau. Prenons par exemple un semi-conducteur intrinsèque : une augmentation de la température va augmenter la quantité d'électrons et de trous, ce qui va diminuer la résistance du semi-conducteur. Voyons ce mécanisme plus en détails. Les semi-conducteurs peuvent conduire le courant grâce à deux mécanismes distincts. Le premier est à l'origine de la résistance du matériau et se décrit assez bien avec la loi d'Ohm. Il donne naissance à un courant appelé courant de dérive. Le second permet d'homogénéiser la densité de charge dans le matériau, celles-ci tendant à se répartir uniformément dans l'espace. Cela induit un déplacement de charge appelé courant de diffusion. Le courant dans un semi-conducteur est la somme d'un courant de dérive <math>I_{derive}</math> et du courant de diffusion <math>I_{diffusion}</math>. : <math>I_{total} = I_{derive} + I_{diffusion}</math> ===Le courant de dérive=== Le premier, appelé le '''courant de dérive''', est aussi présent chez les conducteurs normaux (comprendre les métaux et électrolytes). Il apparait quand on soumet le semi-conducteur à une tension/un champ électrique et sa valeur dépend de cette première ainsi que de la résistance du matériau. C'est lui qui intervient dans la loi d'Ohm pour les conducteurs. La conductivité d'un matériau, définie pour le courant de dérive, dépend de la densité de porteurs de charges <math>n</math> et de leur mobilité <math>\mu</math>. : <math>\sigma = n \cdot q \cdot \mu</math> Cette équation nous dit que plus le nombre de porteurs de charges d'un matériau est grand, plus sa conductivité sera importante. On retrouve donc ce qu'on a dit plus haut : les conducteurs le sont parce qu'ils ont un grand nombre de porteurs de charges, alors que les isolants le sont parce qu'ils n'en ont pas. Pour les semi-conducteurs, la situation est intermédiaire : le nombre de porteurs de charges est très faible en temps normal, mais augmente fortement avec la température, l’éclairement, ou d'autres paramètres physiques. Pour les semi-conducteurs, cette formule s'applique partiellement (il faut rajouter un second courant dit de diffusion qui provient de la concentration des porteurs de charges). Quoiqu’il en soit, on a un courant de trous et un autre courant d'électrons. Ces deux types de charges ont certes la même charge, mais leur densité n'est pas la même, pas plus que leurs mobilités <math>\mu_e</math> et <math>\mu_p</math>. En faisant la somme des deux courants, on a : : <math>\sigma = ( n_e \cdot q \cdot \mu_e ) + ( n_p \cdot q \cdot \mu_p ) </math> : <math>\sigma = q \cdot ( n_e \cdot \mu_e + n_p \cdot \mu_p ) </math> ===Le courant de diffusion=== Outre le courant de dérive vu dans le section précédente, les semi-conducteurs sont parcourus par un second type de courant. Celui-ci, le '''courant de diffusion''', vient d'une différence de concentration des porteurs de charges. Ceux-ci ne sont pas forcément répartis de manière homogène dans le matériau, du fait des hasards de la formation des paires électrons-trous. Il arrive donc que les électrons soient plus nombreux à un endroit du matériau que dans d'autres. Dit autrement, la concentration en électrons libres et/ou en trous n'est pas uniforme et varie dans le matériau. Mais ces différences de concentration se traduisent par une répartition inhomogène des charges : le matériau sera chargé négativement là où il y a un excès d'électrons, positivement là où il en manque, en suivant le gradient de concentration. Ce mouvement de charges donne naissance à un courant dit de diffusion, régit par les lois physiques de la diffusion et décrit par l'équation de la diffusion : * j est la densité de courant en un point du matériau ; * e est la charge électrique d'un électron/trou ; * D est un coefficient appelé coefficient de diffusion ; * <math>\frac{d \rho}{dx}</math> est le gradient de concentration des charges. Voici l'équation pour le courant d'électrons libres : : <math>j = e \cdot D \cdot \frac{d \rho}{dx}</math> Et voici l'équation pour les trous : : <math>j = - e \cdot D \cdot \frac{d \rho}{dx}</math> Il existe une relation entre le coefficient de diffusion et la mobilité <math>\mu</math> des porteurs de charge. Celle-ci est appelée la relation d'Einstein : : <math>D = \mu \cdot \frac{K T}{e} = \mu \cdot V_T</math> Le terme : <math>V_T = \frac{K T}{e}</math> est appelé la '''tension thermique'''. <noinclude> {{NavChapitre | book=Électronique | prev=Les oscillateurs (générateurs de fréquence) | prevText=Les oscillateurs (générateurs de fréquence) | next=Les jonctions PN | nextText=Les jonctions PN }}{{autocat}} </noinclude> 8ckl5s60macdtu4hppqzg2yqvqn53il Python pour le calcul scientifique/Graphiques 0 72920 773904 772233 2026-10-02T08:16:24Z Cdang 1202 orth 773904 wikitext text/x-wiki Rappel : les programmes commencent par : <syntaxhighlight lang="python"> #!/usr/bin/python3 import numpy as np import matplotlib.pyplot as plt </syntaxhighlight> Nous avons déjà vu un premier tracé élémentaire : ''[[../Découverte de Python et de Jupyter#Premier tracé graphique|Découverte de Python et de Jupyter &gt; Premier tracé graphique]]'' ; nous en présentons un autre plus loin : ''[[../Statistiques#Fréquence, histogramme|Statistiques &gt; Fréquence, histogramme]]''. {| class="wikitable" border="0" |- | <syntaxhighlight lang="python"> x = np.arange(0, 2, 0.1) y = x**2 plt.plot(x, y, label="y = x^2") plt.xlabel("x") plt.ylabel("y") plt.title("Graphes de fonctions") plt.legend() plt.show() </syntaxhighlight> | [[Fichier:Graphe fct carre Python Matplotlib Jupyter.png|300px]] |- | <syntaxhighlight lang="python"> M = np.random.randn(50) plt.plot(M, np.ones_like(M), "|") plt.hist(M, bins=10, density=1) plt.show() </syntaxhighlight> | [[Fichier:Histogramme loi normale 50 ech numpy pyplot.png|300px]] |} Nous allons approfondir le sujet. == Concepts de base == === Exemple === [[Fichier:Nomenclature figure python matplotlib fct trigo.svg|thumb|upright=2.5|Nom des différents objets constituant une figure de Python/Matplotlib.]] Considérons un tracé un peu plus avancé : il consiste en deux graphiques représentant les graphes des fonctions cosinus et sinus. Voici deux manières de le réaliser : <syntaxhighlight lang="python"> x = np.linspace(0, 2*np.pi, 50) ycos = np.cos(x) ysin = np.sin(x) fig, liste_axes = plt.subplots(2, 1, layout="constrained") fig.suptitle("Fonctions trigonométriques") liste_axes[0].plot(x, ycos) liste_axes[0].set_title("cosinus") liste_axes[0].set_xlabel("x") liste_axes[0].set_ylabel("y") liste_axes[1].plot(x, ysin) liste_axes[1].set_title("sinus", pad = 5) liste_axes[1].set_xlabel("x") liste_axes[1].set_ylabel("y") plt.savefig("fonctions_trigo_pyplot.svg", format="svg") </syntaxhighlight> Ou bien <syntaxhighlight lang="python"> x = np.linspace(0, 2*np.pi, 50) ycos = np.cos(x) ysin = np.sin(x) plt.subplots(layout="constrained") plt.suptitle("Fonctions trigonométriques") plt.subplot(2, 1, 1) plt.plot(x, ycos) plt.title("cosinus") plt.xlabel("x") plt.ylabel("y") plt.subplot(2, 1, 2) plt.plot(x, ysin) plt.title("sinus", pad = 5) plt.xlabel("x") plt.ylabel("y") </syntaxhighlight> === Analyse === ==== Première manière ==== La création de deux graphiques se fait avec la commande <code lang="python">plt.subplot()</code> à laquelle on indique le fait qu'il y a 2 lignes et 1 colonne ; le paramètre <code lang="python">layout="constrained"</code> permet d'avoir une meilleure mise en page. Cette commande retourne deux identifiants : <code lang="python">fig</code>, l'identifiant de la figure, et <code lang="python">liste_axes</code>, les identifiants des deux sous-figures. La méthode <code lang="python">.suptitle()</code> permet de créer un titre général. Pour chaque sous-figure (classe {{lang|en|<code>Axes</code>}}, que l'on pourrait traduire par « système d'axes »), on utilise les méthodes <code lang="python">.plot()</code>pour tracer une courbe, <code>.set_title()</code> pour lui donner un nom et les méthodes <code lang="python">.set_xlabel()</code> et <code lang="python">.set_ylabel()</code> pour les titres des axes. La fonction <code lang="python">plt.savefig()</code> permet d'enregistrer la figure dans un fichier, ici au format [[Découvrir le SVG|SVG]]. Au lieu d'utiliser une matrice <code>liste_axes</code>, on peut aussi utiliser un n-uplet : <syntaxhighlight lang="python"> fig, (axes0, axes1) = plt.subplots(2, 1, layout="constrained") […] axes0.plot(x, ycos) […] axes1.plot(x, ysin) </syntaxhighlight> L'image est constituée de plusieurs objets. Il y en a de deux types : * les contenants qui permettent la mise en page : <code lang="python">Figure</code> (la figure)<ref>{{lien web |url=https://matplotlib.org/stable/api/figure_api.html |titre=matplotlib.figure |site=Matplotlib.org |consulté le=2022-05-03}}.</ref> et <code>Axes</code> (les systèmes d'axes)<ref>{{lien web |url=https://matplotlib.org/stable/api/axes_api.html |titre=matplotlib.axes |site=Matplotlib.org |consulté le=2022-05-03}}.</ref> ; * les primitives : ce sont les objets qui sont tracés à l'écran, essentiellement les <code>Line2D</code> (les courbes)<ref>{{lien web |url=https://matplotlib.org/stable/api/_as_gen/matplotlib.lines.Line2D.html |titre=matplotlib.lines.Line2D |site=Matplotlib.org |consulté le=2022-05-03}}.</ref>. La <code lang="python">Figure</code> est un grand contenant, c'est lui qui contient tout le dessin ; on parle parfois de « toile » ''({{lang|en|canvas}})''. Les <code>Axes</code> sont les différentes sous-figures ; il y en a ici 2. Les <code lang="python">axis</code> (« axe » en français) — à ne pas confondre avec le terme <code lang="python">Axes</code> précédent — sont au nombre de deux par <code lang="python">Axes</code> : l'axe de ''x'' (<code lang="python">XAxis</code>) et l'axe des ''y'' (<code lang="python">YAxis</code>). ==== Seconde manière ==== Chaque graphique est créé par la fonction <code lang="python">plt.subplot(2, 1, 1)</code>. Les paramètres sont trois nombres : le nombre de lignes du tableau de figures, le nombre de colonnes de ce tableau, et le numéro de la cellule au sein du tableau en suivant le sens de lecture latin (de gauche à droite et de haut en bas). La création des graphiques est précédée de deux fonctions : <code lang="python">plt.subplots(layout="constrained")</code> qui impose une répartition harmonieuse des graphiques, et <code lang="python">plt.suptitle()</code> qui permet d'écrire un titre général. Pour chaque graphique, on a les fonctions : * <code lang="python">plt.plot()</code> qui tracent le graphique ; * <code lang="python">plt.title()</code> qui donnent un titre à chaque graphique ; * <code lang="python">plt.xlabel()</code> et <code lang="python">plt.ylabel()</code> qui nomment les axes. === Styles de codage « pyplot » et « OO » === On voit qu'il y a aussi deux styles de codage, deux manières de rédiger. La première est la manière « pyplot » : la figure est décrite comme un automate fini ''({{lang|en|state machine}})'', elle ne peut avoir qu'un nombre fini d'états et les fonctions font des transitions entre deux états. On utilise aussi l'expression « à la manière de Matlab ». On utilise des fonctions de l'espace de nom <code lang="python">matplotlib.pylot (plt)</code>, ici : <code lang="python">plt.subplots()</code>, <code lang="python">plt.savefig()</code> et dans les exemples précédents, <code>plt.plot()</code>, <code>plt.hist()</code lang="python">, <code lang="python">plt.xlabel()</code>, <code lang="python">plt.ylabel()</code>, <code lang="python">plt.title()</code>, <code lang="python">plt.legend()</code> et <code lang="python">plt.show()</code>. La seconde manière est la manière « orientée objet » (OO) : comme nous avons défini des objets (la <code lang="python">Figure</code>, les <code lang="python">Axes</code>), nous pouvons utiliser des méthodes spécifiques à ces classes. C'est ainsi que nous avons les méthodes <code lang="python">fig.suptitle()</code>, <code lang="python">liste_axes[:].plot()</code>, <code lang="python">.set_title()</code>, <code lang="python">.set_xlabel()</code> et <code lang="python">.set_ylabel()</code>. Dans la manière OO, il faut donc connaître les identifiants des objets pour pouvoir utiliser leurs méthodes. La manière la plus simple est de créer explicitement les objets et d'assigner leur identifiant à des variables à cette occasion, par exemple : <syntaxhighlight lang="python"> fig, sysAxes = plt.subplots(1, 1) courbe = sysAxes.plot(x, y) </syntaxhighlight> ou bien <syntaxhighlight lang="python"> fig = plt.figure() sysAxes = fig.add_subplot(1, 1, 1) courbe = sysAxes.plot(x, y) </syntaxhighlight> ou encore <syntaxhighlight lang="python"> fig = plt.figure() sysAxes = plt.axes() courbe = sysAxes.plot(x, y) </syntaxhighlight> L'autre manière consiste à laisser pyplot tout créer tout seul, puis à récupérer les identifiants : <syntaxhighlight lang="python"> plt.plot(x, y) fig = plt.gcf() # get current figure sysAxes = plt.gca() # get current axes courbe = sysAxes.lines[0] </syntaxhighlight> ou bien <syntaxhighlight lang="python"> plt.plot(x, y) fig = plt.gcf() # get current figure sysAxes = fig.axes[0] courbe = sysAxes.lines[0] </syntaxhighlight> == Mise en forme d'une courbe == Considérons une courbe unique. Nous pouvons changer l'apparence de la courbe en passant des paramètres lors de sa création. La première manière consiste à utiliser une chaîne de caractères comprenant : * une lettre pour la couleur : <code>k</code> pour le noir ''({{lang|en|blac'''k''')}}'', <code>y</code> pour jaune ''({{lang|en|yellow)}}''… * un ou deux caractères pour le type de trait : <code>-</code> pour un trait continu, <code>--</code> pour un trait discontinu… * un caractère pour la forme des marqueurs : <code>o</code> pour des ronds, un <code>s</code> pour un carré ''({{lang|en|square)}}'', un <code>d</code> pour un losange ''({{lang|en|diamond}})'', un <code>v</code> ou un <code>^</code> pour un triangle pointe en bas ou en haut… Par exemple : <syntaxhighlight lang="python"> plt.plot(x, y, "r--") # trait discontinu rouge axes0 = plt.axes() axes0.plot(x, y, "go") # cercles verts </syntaxhighlight> {| class="wikitable" |+ Caractères de mise en forme de courbe |- ! colspan="2" scope="col" | Couleur ! colspan="2" scope="col" | Marqueur |- ! scope="col" | Caractère || Couleur ! scope="col" | Caractère || Symbole |- | <code>b</code> || bleu | <code>.</code> || point • |- | <code>g</code> || vert | <code>,</code> || point d'un pixel ⋅ |- | <code>r</code> || rouge | <code>o</code> || cercle ○ |- | <code>c</code> || cyan | <code>v</code> || triangle pointe en bas ▽ |- | <code>m</code> || magenta | <code>^</code> || triangle pointe en haut △ |- | <code>y</code> || jaune | <code>&lt;</code> || triangle pointe à gauche ◁ |- | <code>k</code> || noir | <code>&gt;</code> || triangle pointe à droite ▷ |- | <code>w</code> || blanc | <code>s</code> || carré □ |- ! colspan="2" scope="col" | Style de ligne | <code>p</code> || pentagone |- ! scope="col" | Caractère || Style | <code>1</code> à <code>4</code> || étoile à trois branches de différentes orientations |- | <code>-</code> || ligne continue | <code>*</code> || étoile ✭ |- | <code>--</code> || ligne discontinue | <code>h</code>, <code>H</code> || hexagone sur la pointe ⬢ ou sur le plat ⬣ |- | <code>-.</code> || trait mixte | <code>+</code> || croix droite + |- | <code>:</code> || trait pointillé | <code>x</code> || croix de saint André × |- | || | <code>D</code>, <code>d</code> || losange carré ◇ ou étroit ◊ |- | || | <code><nowiki>|</nowiki></code> || trait vertical <nowiki>|</nowiki> |- | || | <code>_</code> || trait horizontal — |} Notez la différence entre <code>".-"</code> qui affiche à la fois un marqueur point et un trait continu, et <code>"-."</code> qui crée un trait mixte. '''Méthodes avancées''' Il est possible de passer les paramètres de manière plus explicite : <syntaxhighlight lang="python"> plt.plot(x, y, color="b", linestyle="dashed", marker="o") </syntaxhighlight> L'attribut <code>color</code> peut être spécifié de plusieurs manières : * par une lettre, comme ci-dessus ; * par un mot, le nom de la couleur ; il y a une cinquantaine de nom définis comme par exemple <code>blue</code>, <code>lightblue</code>, <code>darkblue</code>, <code>grey</code>, <code>chocolate</code>, <code>fuschia</code><ref>voir {{lien web | url = https://matplotlib.org/tutorials/colors/colors.html | titre = Specifying colors | site = Matplotlib | lang = en | consulté le = 2019-03-30 }}.</ref>… * avec le nom de la couleur dans la palette T10 ; les dix noms sont <code>tab:blue</code>, <code>tab:orange</code>, <code>tab:green</code>, <code>tab:red</code>, <code>tab:purple</code>, <code>tab:brown</code>, <code>tab:pink</code>, <code>tab:gray</code>, <code>tab:olive</code> et <code>tab:cyan</code> ; ce sont les couleurs utilisées par défaut ; * par un code RVB, les composante R, V et B étant indiquées en hexadécimal entre <code>00</code> et <code>FF</code> : <code>color = "#00FF00"</code> (bleu) ; on peut utiliser le canal alpha en quatrième composante (RVBA) : <code>color = "#00FF0080"</code> ; la composante A va de <code>00</code> pour du parfaitement transparent à <code>FF</code> pour du totalement opaque. L'attribut <code>linestyle</code> peut être indiqué par des caractères comme précédemment ; on peut également utiliser les noms : <code>dashed</code> (discontinu), <code>dashdot</code> (mixte), <code>dotted</code> (pointillés), <code>solid</code> (continu) ou bien <code>None</code> ou une chaîne vide (pas de tracé). On peut ajouter d'autres attributs, en particulier * <code>linewidth</code> ou <code>lw</code> qui définit l'épaisseur du trait ; * <code>linestyle</code> ou <code>ls</code> qui définit le style du trait ; peut prendre les valeurs <code>"solid"</code> (<code>"-"</code>), <code>"dotted"</code> (<code>":"</code>), <code>"dashed"</code> (<code>"--"</code>) ou <code>"dashdot"</code> (<code>"-."</code>) ;<br /> le paramètre peut aussi prendre un n-uplet <code>(décalage, (longeur, espace))</code> ou <code>(décalage, (longeur1, espace1, loongueur2, espace2))</code> (une longueur de 1 désigne un point) ; * <code>dashes</code> : permet de définir un trait discontinu avec un doublet (2-uplet) indiquant le nombre de points affichés et le nombre de points non-affichés, par exemple <code>dashes=(4, 2)</code> ; on peut alterner plusieurs couples comme <code>dashes=(8, 2, 4, 2)</code> * <code>markeredgecolor</code> : couleur du trait du symbole marqueur ; * <code>markeredgewidth</code> : épaisseur du trait du symbole marqueur (en points) ; * <code>markerfacecolor</code> : couleur de remplissage du symbole marqueur ; * <code>fillstyle</code> : permet de ne remplir qu'une moitié du symbole marqueur ; prend les valeurs <code>top</code>, <code>right</code>, <code>bottom</code> et <code>left</code> pour indiquer la moitié remplie ; * <code>markersize</code> : taille du symbole marqueur (en points). Avec le style de codage orienté objet, cela donne : <syntaxhighlight lang="python"> courbe = plt.plot(x, y) courbe[0].set_linestyle("--") # ou bien courbe, = plt.plot(x, y) courbe.set_linestyle("--") # ou encore plt.plot(x, y) axes = plt.gca() courbe = axes.lines[0] courbe.set_linestyle("--") </syntaxhighlight> Les principales méthodes des objets Line2D sont : <code>.set_color()</code>, <code>.set_linestyle()</code>, <code>.set_linewidth()</code>, <code>.set_dashes()</code>, <code>.set_marker()</code> et tous les <code>.set_marker…()</code> (<code>edgecolor</code>, <code>edgewidth</code><code>facecolor</code>, <code>size</code>). ; Ressources * {{lien web | url = https://matplotlib.org/tutorials/introductory/pyplot.html | titre = Pyplot tutorial | site = Matplotlib | lang = en | consulté le = 2019-03-30 }} * {{lien web | url = https://matplotlib.org/api/_as_gen/matplotlib.pyplot.plot.html?highlight=plot#matplotlib.pyplot.plot | titre = matplotlib.pyplot.plot | site = Matplotlib | lang = en | consulté le = 2019-03-30 }} * {{lien web | url = https://matplotlib.org/api/_as_gen/matplotlib.lines.Line2D.html#matplotlib.lines.Line2D.set_linestyle | titre = matplotlib.lines.Line2D.set_linestyle | site = Matplotlib | lang = en | consulté le = 2019-03-30 }} == Annotations == Nous parlons ici le fait de placer des annotations sur une figure et non pas la notion d'annotation en programmation. Il est possible de placer du texte avec la commande <code>plt.text(x, y, chaine)</code> où (''x'', ''y'') sont les coordonnées, dans le repère du tracé, où l'on veut placer le texte, et ''chaine'' est une chaîne de caractères. On peut modifier l'orientation de ce texte avec le paramètre <code lang="python">rotation</code> exprimé en degrés, par exemple : <syntaxhighlight lang="python"> plt.text(0, 0, "origine", rotation=90) </syntaxhighlight> [[Fichier:Pyplot annotate arrowstyle.svg|vignette|Styles de flèche avec la fonction <code>plt.annotate()</code>.]] Par ailleurs, la classe <code>Axes</code> dispose d'une méthode <code>.annotate()</code> ; on peut aussi utiliser la fonction <code>plt.annotate()</code>.. Dans sa forme la plus simple, on indique une chaîne correspondant à l'étiquette et un doublet (2-uplet) ou une liste indiquant les coordonnées où placer l'étiquette. Par exemple : <syntaxhighlight lang="python"> x = np.linspace(0, 2*np.pi, 50) ycos = np.cos(x) courbe = plt.plot(x, ycos) fig = plt.gcf() # get current figure sysAxes = plt.gca() # get current axes ymin = ycos.min() # trouve la veleur minimale de ycos imin = ycos.argmin() # trouve l'indice correspondant au minimum xmin = x[imin] sysAxes.annotate("min", (xmin, ymin)) </syntaxhighlight> Telle quelle, l'étiquette empiète sur la courbe. On peut placer l'étiquette à des coordonnées particulières avec l'attribut <code>xytext</code> cet attribut reçoit un doublet ou une liste de coordonnées (''x'', ''y''). Par ailleurs, on peut relier l'étiquette au point visé avec l'attribut <code>arrowsprop</code> (''{{lang|en|arrow's properties}}'', propriétés de la flèche). Cet attribut reçoit un dictionnaire de propriétés, <code>arrowsprop=dict(…)</code> ou bien <code>arrowsprop={…}</code>. Par exemple, pour avoir simplement un trait, ce dictionnaire contient <code>arrowstyle="-"</code> ; pour une flèche, <code>arrowstyle="->"</code> ou <code>arrowstyle="<-"</code>, et pour une flèche double <code>arrowstyle="<->"</code>. Par exemple, avec l'exemple précédent : <syntaxhighlight lang="python"> sysAxes.annotate("min", (xmin, ymin), xytext=(xmin + 0.5, ymin + 0.5), arrowprops=dict(arrowstyle="->")) # ou bien sysAxes.annotate("min", (xmin, ymin), xytext=(xmin + 0.5, ymin + 0.5), arrowprops={"arrowstyle":"->"}) </syntaxhighlight> Les styles de flèche ''({{lang|en|arrowstyle}})'' disponibles sont : * <code>-</code> : trait simple ; * <code>&lt;-</code>, <code>-&gt;</code>, <code>&lt;-&gt;</code> : flèches ; * <code>&lt;|-</code>, <code>-|&gt;</code>, <code>&lt;|-|&gt;</code> : têtes triangulaires ; * <code>]-</code>, <code>-[</code>, <code>]-[</code> : crochets ; * <code>|-|</code> : barre ; * <code>]-&gt;</code>, <code>&lt;-[</code> : crochet d'un côté, flèche de l'autre ; * <code>simple</code>, <code>fancy</code>, <code>wedge</code> : variantes de <code>-&gt;</code>. ; Ressource : {{lien web | url = https://matplotlib.org/api/_as_gen/matplotlib.axes.Axes.annotate.html | titre = matplotlib.axes.Axes.annotate | lang = en | site = Matplotlib | consulté le = 2019-04-07 }} == Autres exemples de tracés 2D == [[Fichier:Sinus trace matplotlib escalier batons fleches.svg |vignette |Trois types de tracés de la fonction sinus avec Python/Matplotlib]] Par exemple avec : <syntaxhighlight lang="python"> x = np.linspace(0, 2*np.pi, 20) y = np.sin(x) fig, listeAxes = plt.subplots(3, 1, layout="constrained") # Tracé en escalier listeAxes[0].step(x, y) # tracé en bâtons listeAxes[1].bar(x, y, width = 0.03) # Tracé en flèches listeAxes[2].quiver(x[:-1], y[:-1], np.diff(x), np.diff(y), scale_units="xy", scale=1, angles="xy", width=0.02) </syntaxhighlight> == Gestion des échelles == Pour fixer les valeurs extrêmes des axes, on utilise les commandes <code lang="python">plt.xlim(''min'', ''max'')</code> et <code lang="python">plt.ylim(''min'', ''max'')</code>. Pour avoir une échelle logarithmique, respectivement en ''x'' et en ''y'', on utilise les commandes <code lang="python">plt.xscale("log")</code> et <code lang="python">plt.yscale("log")</code>. Ces fonctions peuvent recevoir les paramètres suivants : * <code>linear</code> : échelle linéaire (valeur par défaut) ; * <code>log</code> : échelle logarithmique ; * <code>symlog</code> : échelle logarithmique symétrique, c'est-à-dire avec la transformation <math>\mathrm{Y} = \sgn(y) \cdot \log_{10}(1 + |y/\mathrm{C}|)</math> avec C = 1/ln(10) ; nous avons ainsi des valeurs positives et négative sans avoir la singularité en 0 ; * <code>logit</code> : l'échelle est transformée par la fonction [[w:fr:Logit|logit]]. On peut superposer une grille avec la commande <code lang="python">plt.grid(True)</code>. S'il y a trop de graduations sur un axe, on peut réduire le nombre de graduations avec : <syntaxhighlight lang="python"> from matplotlib.ticker import NullFormatter # […] plt.gca().yaxis.set_minor_formatter(NullFormatter()) </syntaxhighlight> On peut imposer les valeurs graduées : <syntaxhighlight lang="python"> plt.gca().set_xticks([0, 1, 2, 3]) </syntaxhighlight> On peut imposer le fait d'avoir les mêmes échelles en ''x'' ou en ''y'' avec les mots-clefs <code lang="python">sharex</code> et <code lang="python">sharey</code>. Par exemple : <syntaxhighlight lang="python"> fig = plt.figure() ax1 = fig.add_subplot(2, 1, 1) ax2 = fig.add_subplot(2, 1, 2, sharex=ax1) </syntaxhighlight> ou bien <syntaxhighlight lang="python"> fig, axs = plt.subplots(2,1, sharex=True) </syntaxhighlight> Si l'on veut que l'échelle en ''x'' soit la même que l'échelle en ''y'', on peut utiliser la méthode <code lang="python">set_aspect()</code><ref>{{lien web |url=https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.set_aspect.html |titre=matplotlib.axes.Axes.set_aspect |site=Matplotlib.org |consulté le=2022-05-03}}</ref> des objets <code lang="python">axes</code>, par exemple <syntaxhighlight lang="python"> plt.gca().set_aspect("equal", adjustable="box") </syntaxhighlight> Si l'on veut que le graphique soit carré, on utilise le mot-clef <code lang="python">square</code> de l'objet <code lang="python">axis</code> : <syntaxhighlight lang="python"> plt.axis("square") </syntaxhighlight> Si l'on veut que les échelles sur les axes ''x'' et ''y'' soient de pas identiques, par exemple pour qu'un carré soit un carré et un demi-cercle ne soit pas une demie ellipse, on utilise : <syntaxhighlight lang="python"> plt.gca().set_aspect("equal", adjustable="box") </syntaxhighlight> == Plusieurs courbes sur un même système d'axes == Pour mettre plusieurs courbes sur un même système d'axes, on peut soit les créer les unes après les autres : <syntaxhighlight lang="python"> plt.plot(x1, y1) plt.plot(x2, y2) </syntaxhighlight> Soit les mettre dans la même commande : <syntaxhighlight lang="python"> plt.plot(x1, y1, x2, y2) </syntaxhighlight> Matplotlib définit lui-même des couleurs différentes. On peut bien évidemment forcer le style des courbes. On peut également ajouter une étiquette ''({{lang|en|label}})'' qui sera ensuite affichée dans la légende : <syntaxhighlight lang="python"> plt.plot(x1, y1, "k", label="courbe 1") plt.plot(x2, y2, "b", label="courbe 1") plt.legend() # ou bien courbes = plt.plot(x1, y1, "k", x2, y2, "b") courbes[0].set_label("courbe 1") courbes[1].set_label("courbe 2") plt.legend() # ou bien courbes = plt.plot(x1, y1, x2, y2) courbes[0].set(color="k", label="courbe 1") courbes[1].set(color="b", label="courbe 2") plt.legend() </syntaxhighlight> On peut choisir l'emplacement de la légende avec le paramètre <code lang="python">loc</code> qui peut prendre les valeurs <code lang="python">"upper left"</code>, <code lang="python">"upper center"</code>, <code lang="python">"upper right"</code>, <code lang="python">"center left"</code>… la valeur est composée de deux adjectifs : * premier adjectif : ** <code lang="en">center</code> signifie au centre, au milieu, ** <code lang="en">upper</code> signifie en haut, ** <code lang="en">lower</code> signifie en bas ; * second adjectif : ** <code lang="en">center</code> signifie au centre, au milieu, ** <code lang="en">left</code> signifie à gauche, ** <code lang="en">right</code> signifie à droite. Par exemple : <syntaxhighlight lang="python"> plt.legend(loc="upper right") </syntaxhighlight> Si l'on veut des courbes avec des échelles en ''y'' différentes, il faut créer un axe ''x'' « jumeau » ''({{lang|en|twin}})'' avec la méthode <code>.twinx()</code>. Par exemple : <syntaxhighlight lang="python"> fig, axe = plt.subplots(figsize=(5, 5)) plt.suptitle("Chargement") axe2 = axe.twinx() axe.plot(forces) axe.set_xlabel("Étape") axe.set_ylabel("F (N)") axe2.plot(contraintes) axe2.set_ylabel("σ (MPa)") </syntaxhighlight> == Tracé polaire == [[Fichier:Courbe polaire rayon aleatoire python matplotlib.svg |vignette |Exemple de courbe polaire avec Python/Matplotlib.]] [[File:Rose des vents exemple Python.svg|thumb|Rose des vents avec des données arbitraires, utilisé pour un exemple de création de graphique avec le langage Python/Matplotlib.]] Un tracé polaire s'obtient avec la commande <code>plt.polar()</code>, par exemple : <syntaxhighlight lang="python"> theta = np.linspace(0, 2*np.pi, 20) # angle r = 1 + 0.2*np.random.randn(20) # rayon plt.polar(theta, r) # courbe polaire </syntaxhighlight> Voici un exemple avec deux courbes superposées et une mise en forme des graduations et de la légende : <syntaxhighlight lang="python"> #!/usr/bin/python3 import numpy as np import matplotlib.pyplot as plt # ************** # * Constantes * # ************** # positions angulaires angles = 2*np.pi*np.linspace(0, 1, 9) # huitièmes de cercle radDeg = 180/np.pi # conversion radians → degrés etoile = np.array([1, 0.2, 1, 0.2, 1, 0.2, 1, 0.2, 1]) # Tracé de l'étoile vents = np.array([2.85, 3.34, 5.2, 4.025, 4.9, 9.63, 7.5, 2.35, 2.85]) # Fréquence (unité arbitraire) vents = vents/vents.max() # normalisation # *********** # * Traçage * # *********** courbes = plt.polar(angles, etoile, angles, vents) # tracé # mise en forme axes = plt.gca() courbes[0].set(color="gray") courbes[1].set(color="b", label="Fréquence des vents") plt.legend(bbox_to_anchor=(1.45, 1.05), loc="upper right") # affichage à l'extérieur du tracé axes.set_rticks([0.2, 0.4, 0.6, 0.8, 1]) # graduations radiales axes.set_thetagrids(angles=radDeg*angles[0:-1], labels=("E", "NE", "N", "NO", "O", "SO", "S", "SE")) # graduations angulaires plt.title("Rose des vents") plt.savefig("rose_des_vents.svg", format="svg") plt.show() </syntaxhighlight> == Cartes de valeurs et de vecteurs == [[File:Cartes couleur sinFoisSin python matplotlib.svg|thumb|Lignes de niveau et cartes couleur de ''z'' = sin(''x'') × sin(''y'') par Python/Matplotlib.]] Lorsque l'on a une surface dans un espace à trois dimensions, de type ''z'' = ƒ(''x''&htinsp;, ''y''&thinsp;), on peut afficher les données sur la forme d'une carte : * lignes de niveau : on utilise la fonction <code>np.contour()</code> ; * surfaces de niveau : <code>np.contourf()</code> ; * carte de couleurs : <code>np.pcolormesh()</code>. La fonction <code>colorbar()</code> permet d'afficher l'échelle de couleurs. Par exemple : <syntaxhighlight lang="python"> x = np.linspace(0, 2*np.pi, 20) y = x.copy() X, Y = np.meshgrid(x, y) Z = np.sin(X)*np.sin(Y) fig, liste_axes = plt.subplots(2, 2, layout="constrained") im1 = liste_axes[0, 0].contour(X, Y, Z) fig.colorbar(im1, ax=liste_axes[0, 0]) im2 = liste_axes[0, 1].contourf(X, Y, Z) fig.colorbar(im2, ax=liste_axes[0, 1]) im3 = liste_axes[1, 0].pcolormesh(X, Y, Z) fig.colorbar(im3, ax=liste_axes[1, 0]) </syntaxhighlight> La palette de couleurs ''({{lang|en|color map}})'' par défaut est ''viridis''. On peut choisir d'autres palettes de couleur avec le paramètre <code>cmap</code> : <syntaxhighlight lang="python"> im3 = liste_axes[1, 0].pcolormesh(X, Y, Z, cmap="Greens") </syntaxhighlight> Les échelles de couleur disponibles sont données à la page : : {{lien web | url = https://matplotlib.org/api/pyplot_summary.html#matplotlib.pyplot.colormaps | titre = Colors in matplotlib | site = matplotlib | lang = en | consulté le = 2019-04-05 }}. [[Fichier:Champ vecteurs python matplotlib.svg|vignette|Champ de vecteur tracé par Python/Matplotlib.]] On peut aussi faire une carte d'un champ de vecteurs avec la commande <code>plt.quiver()</code> vue précédemment : <syntaxhighlight lang="python"> base = np.arange(10) x, y = np.meshgrid(base, base) ux = -0.1*y uy = 0.1*x plt.quiver(x, y, ux, uy, scale_units="xy", scale = 1, angles="xy") </syntaxhighlight> == Surfaces 3D == [[Fichier:Surface3D sinFoisSin python matplotlib.svg|vignette|Surface ''z'' = sin(''x'') × sin(''y'') tracée avec Python/Matplotlib.]] Il est également possible de tracer des surfaces 3D. Il faut pour cela charger <code>Axes3D</code> du module <code>mpl_toolkits.mplot3d</code> ; cela donne accès au paramètre <code>projection="3d"</code> lorsque l'on définit le système d'axes. Par exemple : <syntaxhighlight lang="python"> from mpl_toolkits.mplot3d import Axes3D # permet d'utiliser l'attribut projection="3d" x = np.linspace(0, 2*np.pi, 20) y = x.copy() X, Y = np.meshgrid(x, y) Z = np.sin(X)*np.sin(Y) plt.figure() ax = plt.axes(projection="3d") ax.contour(X, Y, Z, zdir="z", offset=-1) # projection sur le plan z = -1 ax.plot_surface(X, Y, Z, alpha=0.8) </syntaxhighlight> On peut changer l'angle de vue du système d'axes avec les paramètres <code>elev</code> (élévation) et<code>azim</code> (azimut). Par exemple <syntaxhighlight lang="python"> ax = plt.axes(projection="3d", elev = 10, azim = 90) </syntaxhighlight> == Mise en forme de la figure == La figure dispose de paramètres de mise en forme : * <code lang="python">figsize</code> : liste des dimensions (largeur, hauteur) en pouces (valeur par défaut <code>[6.4, 4.8]</code>) ; * <code lang="python">dpi</code> : résolution en points par pouce (valeur par défaut <code>100</code>) ; * <code lang="python">facecolor</code> : couleur de fond (valeur par défaut <code>white</code>) ; * <code lang="python">edgecolor</code> : couleur du filet de bordure (valeur par défaut <code>white</code>) ; * <code lang="python">layout</code> : définit le placement des objets pour éviter les effets indésirables (par exemple le recouvrement, valeur par défaut <code>none</code>). Par exemple : <syntaxhighlight lang="python"> M = np.random.randn(50) fig = plt.figure(figsize = [10, 6]) plt.plot(M) </syntaxhighlight> == Mise en forme du texte == Si l'on veut utiliser le style LaTeX, on utilise : <syntaxhighlight lang="python"> plt.rcParams["text.usetex"] = True […] plt.xlabel("$x$") </syntaxhighlight> == Dessins == Les dessins portent le nom de « pièces » ''({{lang|en|patch}})''. Les fonctions pour les créer se trouvent dans le module <code>matplotlib.patches</code>. Ces fonctions permettent de ''créer'' les pièces mais pour les ''dessiner'' (c'est-à-dire les mettre sur la « toile »), il faut utiliser la méthode <code>.add_patch()</code>. Par exemple : <syntaxhighlight lang="python"> from matplotlib.patches import Circle, Polygon fig = plt.figure() ax = fig.add_axes([0, 0, 1, 1]) # système d'axes occupant toute la figure ax.set_xlim(0, 2) # axes gradués de 0 à 2 ax.set_ylim(0, 2) # Création des pièces cercle = Circle((0.25, 0.25), 0.1) # cercle de centre (0,25 ; 0,25) et de rayon 0,1 polygone = Polygon([[1.5, 1.5], [2, 1.5], [1.75, 2]]) # polygone à trois sommets (triangle) # Dessin des pièces ax.add_patch(cercle) ax.add_patch(polygone) </syntaxhighlight> Le dessin des pièces une par une est un processus assez long. Il est intéressant de créer une collection et de dessiner cette collection d'un seul tenant. On a pour cela recours au module <code>matplotlib.collections</code>. Le code devient donc : <syntaxhighlight lang="python"> from matplotlib.patches import Circle, Polygon from matplotlib.collections import PatchCollection fig = plt.figure() ax = fig.add_axes([0, 0, 1, 1]) ax.set_xlim(0, 2) ax.set_ylim(0, 2) cercle = Circle((0.25, 0.25), 0.1) polygone = Polygon([[1.5, 1.5], [2, 1.5], [1.75, 2]]) coll = PatchCollection((cercle, polygone)) ax.add_collection(coll) </syntaxhighlight> Il est possible de dessiner des chemins ''({{lang|en|path}})''. {{loupe|Découvrir le SVG/Chemins}} On a pour cela recours au module <code>matplotlib.path</code>. Par exemple : <syntaxhighlight lang="python"> from matplotlib.patches import PathPatch from matplotlib.path import Path fig = plt.figure() ax = fig.add_axes([0, 0, 1, 1]) ax.set_xlim(0, 2) ax.set_ylim(0, 2) # Création du chemin chemin = Path([[0.25, 0.5], [0.25, 1.5], [0.75, 1.5]], [Path.MOVETO, Path.CURVE3, Path.CURVE3]) # Création de la pièce à partir du chemin pieceChemin = PathPatch(chemin) # Dessin de la pièce ax.add_patch(pieceChemin) </syntaxhighlight> Ici, nous disons à la « plume » de se rendre ''({{lang|en|move to}})'' au point de coordonnées (0,25 ; 0,25), puis de tracer une courbe de Bézier quadratique ''({{lang|en|curve 3}})'' jusqu'au point (0,75 ; 1,5) avec comme point de contrôle intermédiaire (0,25 ; 1,5). == Transformation des objets graphiques == Il est possible de faire subir des transformations linéaires aux objets graphiques<ref>{{lien web |url=https://matplotlib.org/stable/api/transformations.html |titre=matplotlib.transforms |site=Matplotlib.org |consulté le=2023-04-14}}.</ref>. On utilise pour cela le module <code lang="python">matplotlib.transforms</code> (au pluriel) et l'attribut <code lang="python">transform</code> (au singulier) des commandes pyplot. Par exemple, pour tourner un objet de 90° : <syntaxhighlight lang="python"> import numpy as np import matplotlib.pyplot as plt import matplotlib.transforms as transforms donnees = np.random.randn(20) # points au hasard base = plt.gca().transData # base de la transformation rotation = transforms.Affine2D().rotate_deg(90) # rotation de 90° plt.plot(donnees, transform = rotation + base) </syntaxhighlight> La transformation <code>transforms.Affine2D()</code> a normalement pour argument une matrice 3 × 3 classique en géométrie. Lorsque l'on est en deux dimensions, cette matrice a la forme : <math>\begin{pmatrix} a & b & 0 \\ c & d & 0 \\ 0 & 0 & 1 \end{pmatrix} </math> {{loupe|w:Matrice de transformation}} Les méthodes d'<code>Affine2D()</code> permettent de générer automatiquement cette matrice. Par exemple, <code>transforms.Affine2D().rotate(θ)</code> (angle θ en radians ; on utilise <code>.rotate_deg()</code> pour des angles en degrés) crée la matrice de rotation : <math>\begin{pmatrix} \cos(\theta) & -\sin(\theta) & 0 \\ \sin(\theta) & \cos(\theta) & 0 \\ 0 & 0 & 1 \end{pmatrix} </math> On peut donner directement la matrice M (de type <code>np.array()</code> avec <code>transforms.Affine2D().set_matrix(M)</code>. On peut indiquer les valeurs sous la forme d'une liste avec <code>transforms.Affine2D().from_values(a, c, b, d, 0, 0)</code> (notez l'ordre des paramètres). == Créer un GIF animé == Il existe plusieurs modules permettant de créer un GIF animé dans Python. === Avec le module PIL (Python imaging library) === La première étape consiste à créer un n-uplet contenant les noms de tous les fichiers image, dans l'ordre ; nous l'appelons <code>imgs</code>. Puis, nous créons un n-uplet contenant les pointeurs vers les images, ouverte avec <code>PIL.Images.open()</code> ; nous l'appelons <code>trames</code>. Nous créons le fichier GIF animé en utilisant la méthode <code>.save()</code> du n-uplet <code>trames</code> <syntaxhighlight lang="python"> from PIL import Image imgs=[] # initialisation du n-uplet for i in range(10): # nom des images : "fichieri.png", i étant un entier ("fichier0.png", "fichier1.png" etc.) imgs.append("fichier"+str(i)+".png") trames = [] for i in imgs: nouv_trame = Image.open(i) trames.append(nouv_trame) trames[0].save("animation.gif", format="GIF", append_images=trames[1:], save_all=True, duration=300, # durée en milliseconde loop=0) # pour boucler ; </syntaxhighlight> ; Ressources * https://pillow.readthedocs.io/ == Les couches de <code>matplotlib</code> == : {{lien web | url=https://matplotlib.org/tutorials/intermediate/artists.html | titre = Artist tutorial | lang = en | site = Matplotlib | consulté le = 2019-03-29 }} : {{lien web | url=https://matplotlib.org/api/artist_api.html | titre = Artist API | lang = en | site = Matplotlib | consulté le = 2019-03-29 }} Pour bien comprendre la documentation du module <code>matplotlib</code>, il faut comprendre sa structure en couches. Cela n'est pas indispensable pour l'utilisation élémentaire du module, mais connaître ce vocabulaire devient important pour une utilisation avancée. Le module <code>matplotlib</code> est organisé en trois couches : * la toile ({{lang|en|<code>FigureCanvas</code>}}) : c'est l'espace sur lequel le tracé est fait ; * le restituteur ({{lang|en|<code>Renderer</code>}}) : c'est l'objet chargé d'effectuer les tracés sur la toile ; * les artistes ({{lang|en|<code>Artist</code>}}) : ce sont les classes des objets destinés à être tracés. Il faut distinguer le tracé ''({{lang|en|plot}})'', qui consiste à traduire les données en objets à tracer en utilisant les artistes, et la publication ''({{lang|en|show}})'' qui consiste à afficher les tracés à l'écran ou à les écrire dans un fichier. La publication est le rôle de la « face cachée » ''({{lang|en|backend}})''. Cette structure en trois couches facilite la tâche de l'utilisateur : vous n'avez à vous concentrer qu'aux données et au type et style de tracé (en manipulant les artistes), la face cachée s'occupe de gérer la partie logicielle et matérielle (différence de système d'exploitation, de carte graphique, de format de fichier). La face cachée est donc une couche « abstraite », que l'utilisateur ne voit quasiment jamais. Dans les cas les plus simples, l'utilisation d'un artiste provoque aussi sa publication : si l'on fait un <code>.plot()</code>, la courbe s'affiche à l'écran. Mais dans certains cas, il faut indiquer à Python de publier le graphique ; par exemple, s'il y a des opérations de tracé longues à faire, on a intérêt à tout afficher à la fin, une fois toutes les opérations faites. C'est la raison d'être des fonctions et méthodes <code>.show()</code> ou <code>.draw()</code>. On distingue ainsi deux types d'artistes : les contenants et les primitives. Nous avons ainsi l'organisation suivante : * face cachée ({{lang|en|<code>backend</code>}}) ** toile ({{lang|en|<code>FigureCanvas</code>}}) ** restituteur ({{lang|en|<code>Renderer</code>}}) * artistes ({{lang|en|<code>Artist</code>}}) ** conteneurs ({{lang|en|<code>container</code>}}) *** figure ({{lang|en|<code>Figure</code>}}) **** systèmes d'axes ({{lang|en|<code>Axes</code>}}) ***** axes ({{lang|en|<code>axis</code>}}) ****** graduations ({{lang|en|<code>ticks</code>}}) ***** légendes ({{lang|en|<code>legend</code>}}) **** … ** primitives ({{lang|en|<code>primitive</code>}}) *** courbes ({{lang|en|<code>Line2D</code>}}) *** textes ({{lang|en|<code>Text</code>}}) *** bords ({{lang|en|<code>Spine</code>}}) *** pièces ({{lang|en|<code>Patch</code>}}) *** … == Notes et références == {{références}} ---- [[../Éléments de programmation|Éléments de programmation]] &lt; [[../|↑]] &gt; [[../Manipulation de matrices|Manipulation de matrices]] [[Catégorie:Python pour le calcul scientifique (livre)]] 0pcxywdh0w6p8ee65k3imas2o09bvxq Formation musicale/Harmonie 0 75583 773903 770344 2026-10-02T07:56:40Z Cdang 1202 /* Voir aussi */ biblio 773903 wikitext text/x-wiki {{Bases de solfège}} <span style="font-size:25px;">6. Harmonie</span> L'harmonie désigne les notes jouées en même temps, soit plusieurs instruments jouant chacun une note, soit un instrument jouant un accord (instrument dit polyphonique). == Première approche == L'exemple le plus simple d'harmonie est sans doute la chanson en canon : c'est un chant polyphonique, c'est-à-dire à plusieurs voix, chaque voix chantant la même chose en décalé. Prenons par exemple ''Vent frais, vent du matin'' (la version originale est ''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609) : [[Fichier:Vent frais vent du matin.svg|class=transparent|center|Partition de ''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]] [[Fichier:Vent frais vent du matin.midi|vignette|''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]] nous voyons que les voix se superposent de manière « harmonieuse ». Les notes de chaque voix se correspondent point par point (avec un retard), c'est donc un type d'harmonie polyphonique appelé « contrepoint ». Considérons la première note de la mesure 6 pour chaque voix. Nous avons la superposition des notes ''ré''-''fa''-''la'' (du grave vers l'aigu) ; la superposition de notes jouées ou chantées ensembles s'appelle un accord. Cet accord ''ré''-''fa''-''la'' porte le nom « d'accord parfait de ''ré'' mineur » : * « ''ré'' » car la note fondamentale est un ''ré'' ; * « parfait » car il est l'association d'une tierce, ''ré''-''fa'', et d'une quinte juste, ''ré''-''la'' ; * « mineur » car le premier intervalle, ''ré''-''fa'', est une tierce mineure. Considérons maintenant un chant accompagné au piano. Le piano peut jouer plusieurs notes en même temps, il peut jouer des accords. [[Fichier:Au clair de le lune chant et piano.svg|class=transparent|center|Deux premières mesure d’Au clair de la lune.]] [[Fichier:Au clair de le lune chant et piano.midi|vignette|Deux premières mesure d’Au clair de la lune.]] L'accord, les notes à jouer simultanément, sont écrites « en colonne ». Lorsqu'on les énonce, on les lit de bas en haut mais le pianiste les joue en pressant les touches du clavier en même temps, de manière « plaquée ». Le premier accord est composé des notes ''do''-''mi''-''sol'' ; il est appelé « accord parfait de ''do'' majeur » car la note fondamentale est ''do'', qu'il est l'association d'une tierce et d'une quinte juste et que le premier intervalle, ''do''-''mi'', est une tierce majeure. Si l'on lit toutes les notes, de la plus grave à la plus aiguë, nous avons : ''do'' (à la clef de ''fa'', main gauche du piano) - ''do'' - ''mi'' - ''sol'' (main droite du piano) - ''do'' (chant) ; nous voyons que la note ''do'' est jouée plusieurs fois, cela ne change pas la qualité de l'accord. == Consonance et dissonance == Les notions de consonance et de dissonance sont culturelles et changent selon l'époque. Nous pouvons néanmoins noter que : * l'accord de seconde, et son renversement la septième, créent des battements, les notes « frottent », c'est un intervalle harmonique dissonant ; mais dans le cas de la septième, comme les notes sont éloignées, le frottement est moins perceptible ; * les accords d'unisson, de tierce, quarte et quinte sonnent agréablement à l'oreille, ils sont consonants. Dans la musique savante européenne, au début du Moyen-Âge, seuls les accords d'unisson, de quarte et de quinte étaient considérés comme consonants, d'où leur qualification de « juste ». La tierce, et son renversement la sixte, étaient perçues comme dissonantes. L'harmonie joue avec les consonances et les dissonances. Dans un premier temps, les harmonies dissonantes sont utilisées pour créer des tensions qui sont ensuite résolues, on utilise des successions « consonant-dissonant-consonant ». À force d'entendre des intervalles considérés comme dissonants, l'oreille s'habitue et certains finissent par être considérés comme consonants ; c'est ce qui est arrivé à la tierce et à la sixte à la fin du Moyen Âge avec le contrepoint, puis à la septième plus tardivement. Il faut ici aborder la notion d'harmonique des notes. [[File:Harmoniques de do.svg|thumb|Les six premières harmoniques de ''do''.]] Lorsque l'on joue une note, on entend d'autres notes plus aigües et plus faibles ; la note jouée est appelée la « fondamentale » et les notes plus aigües et plus faibles sont les « harmoniques ». C'est cette accumulation d'harmoniques qui donne la couleur au son, son timbre, qui fait qu'un piano ne sonne pas comme un violon. Par exemple, si l'on joue un ''do''<sup>1</sup><ref>Pour la notation des octaves, voir ''[[../Représentation_musicale#Désignation_des_octaves|Représentation musicale &gt; Désignation des octaves]]''.</ref> (fondamentale), on entend le ''do''<sup>2</sup> (une octave plus aigu), puis un ''sol''<sup>2</sup>, puis encore un ''do''<sup>3</sup> plus aigu, puis un ''mi''<sup>3</sup>, puis encore un ''sol''<sup>3</sup>, puis un ''si''♭<sup>3</sup>… Ainsi, puisque lorsque l'on joue un ''do'' on entend aussi un ''sol'' très léger, alors jouer un ''do'' et un ''sol'' simultanément n'est pas choquant. De même pour ''do'' et ''mi''. De là vient la notion de consonance. Le statut du ''si''♭ est plus ambigu. Il fait partie des harmoniques qui sonnent naturellement, mais il forme une seconde descendante avec le ''do'', intervalle dissonant. Par ailleurs, on remarque que le ''si''♭ ne fait pas partie de la gamme de ''do'' majeur, contrairement au ''sol'' et au ''mi''. Pour le jeu sur les dissonances, on peut écouter par exemple la ''Toccata'' en ''ré'' mineur, op. 11 de Sergueï Prokofiev (1912). : {{lien web |url=https://www.youtube.com/watch?v=AVpnr8dI_50 |titre=Yuja Wang Prokofiev Toccata |site=YouTube |date=2019-02-26 |consulté le=2021-12-19}} == Contrepoint == Dans le chant grégorien, la notion d'accord n'existe pas. L'harmonie provient de la superposition de plusieurs mélodies, notamment dans ce que l'on appelle le « contrepoint ». Le terme provient du latin ''« punctum contra punctum »'', littéralement « point par point », et désigne le fait que les notes de chaque voix se correspondent. L'exemple le plus connu de contrepoint est le canon, comme ''Frère Jacques'' ou bien ''Vent frais, vent du matin'' présenté ci-dessus : chaque note d'un couplet correspond à une note du couplet précédent. Certains morceaux sont bâtis sur une écriture « en miroir » : l'ordre des notes est inversé entre les deux voix, ou bien les intervalles sont inversés (« mouvement contraire » : une tierce montante sur une voix correspond à une tierce descendante sur l'autre). On peut également citer le « mouvement oblique » (une des voix, le bourdon, chante toujours la même note) et le mouvement parallèle (les deux voix chantent le même air mais transposé, l'une est plus aiguë que l'autre). Nous reproduisons ci-dessous le début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel. [[Fichier:Haendel Sonate en trio re mineur debut canon.svg | vignette | center | upright=2 | Début du second ''Allergo'' de la sonate en trio en ''ré'' mineur de Haendel.]] [[Fichier:Haendel Sonate en trio re mineur debut.midi | vignette | Début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.]] Nous avons mis en évidence la construction en canon avec des encadrés de couleur : sur les quatre premières mesures, nous voyons trois thèmes repris alternativement par une voix et par l'autre. Ce type de procédé est très courant dans la musique baroque. Les procédés du contrepoint s'appliquent également à la danse : * unisson : les danseurs et danseuses font les mêmes gestes en même temps ; * répétition : le fait de répéter une série de gestes, une « phrase dansante » ; * canon : les gestes sont faits avec un décalage régulier d'un danseur ou d'une danseuse à l'autre ; * cascade : forme de canon dans laquelle le décalage est très petit ; * contraste : deux danseur·euses, ou deux groupes, ont des gestuelles très différentes ; * accumulation : la gestuelle se complexifie par l'ajout d'éléments au fur et à mesure ; ou bien le nombre de danseur·euses augmente ; * dialogue : les gestes de danseur·euses ou de groupes se répondent ; * contre-point : la gestuelle d'un ou une danseuse se superpose à la gestuelle d'un groupe ; * lâcher-rattraper : les danseurs et danseuses alternent danse à l'unisson et gestuelles indépendantes. : {{lien web | url=https://www.youtube.com/watch?v=wgblAOzedFc | titre=Les procédés de composition en danse | auteur= Doisneau Sport TV | site=YouTube | date=2020-03-16 | consulté le=2021-01-21 }} {{...}} == Ostinato == L'ostinato, mot italien signifiant « obstiné », c'est le fait de jouer en boucle un passage. L'ostinato peut ainsi former une structure autour de laquelle peut se développer le morceau. Lorsque l'ostinato est joué par l'instrument de basse (basson, violoncelle, viole de gambe basse, flûte à bec basse, main gauche du piano…), on parle de basse obstinée. Les grilles d'accord en jazz sont des ostinatos (ou ostinati) joués par la section rythmique. Exemples : * {{lien web |url=https://www.youtube.com/watch?v=QZpGe5rNJkI&t=41s |titre=Epic Patty Cake Song (I'll Think Of You) |auteur= Kurt Hugo Schneider |site=YouTube |date=2014-08-12 |consulté le=2026-01-24}} : un ostinato de percussions corporelles ; * {{lien web |url=https://www.youtube.com/watch?v=Kv-37-nXC48 |titre=Pow Wow ''Le lion est mort ce soir'' Les Victoires de la Musique 1993 |auteur=Solomon Linda |date=2015-02-02 |consulté le=2026-01-24}} : dans ce chant ''a capella'' (sans accompagnement d'instrument), les voix de basse forment une basse obstinée, sur les syllabes « oëap » durant le couplet et « awimbowé » sur le refrain ; * {{lien web |url=https://www.youtube.com/watch?v=JvNQLJ1_HQ0 |titre=Pachelbel Canon in D Major - the original and best version. |auteur=Voices of Music |site=YouTube |date=2008-09-02 |consulté le=2026-01-24}} : le ''Canon de Pachelbel de ''ré'' mineur'' (''Canon per tre Violini e Basso'', Johann Pachelbel, vers 1680) est construit sur une basse obstinée de huit mesures. == Les accords en général == Initialement, on a des chants polyphoniques, des voix qui chantent chacune une mélodie, les mélodies se mêlant. On remarque que certaines superpositions de notes sonnent de manière plus ou moins agréables, consonantes ou dissonantes. On en vient alors à associer ces notes, c'est-à-dire à considérer dès le départ la superposition de ces notes et non pas la rencontre de ces notes au gré des mélodies. Ces groupes de notes superposées forment les accords. En Europe, cette notion apparaît vers le {{pc|xiv}}<sup>e</sup> siècle avec notamment la ''[[wikipedia:fr:Messe de Notre Dame|Messe de Notre Dame]]'' de Guillaume de Machaut (vers 1360-1365). La notion « d'accord parfait » est consacrée par [[wikipedia:fr:Jean-Philippe Rameau|Jean-Philippe Rameau]] dans son ''Traité de l'harmonie réduite à ses principes naturels'', publié en 1722. === Qu'est-ce qu'un accord ? === Un accord est un ensemble d'au minimum trois notes jouées en même temps. « Jouées » signifie qu'il faut qu'à un moment donné, elles sonnent en même temps, mais le début ou la fin des notes peut être à des instants différents. Considérons que l'on joue les notes ''do'', ''mi'' et ''sol'' en même temps. Cet accord s'appelle « accord de ''do'' majeur ». En musique classique, on lui adjoint l'adjectif « parfait » : « accord parfait de ''do'' majeur ». Nous représentons ci-dessous trois manière de faire l'accord : avec trois instruments jouant chacun une note : [[Fichier:Do majeur trois portees.svg|class=transparent|center|Accord de ''do'' majeur avec trois instruments différents.]] Avec un seul instrument jouant simultanément les trois notes : [[Fichier:Chord C.svg|class=transparent|center|Accord de ''do'' majeur joué par un seul instrument.]] L'accord tel qu'il est joué habituellement par une guitare d'accompagnement : [[Fichier:Do majeur guitare.svg|class=transparent|center|Accord de ''do'' majeur à la guitare.]] Pour ce dernier, nous représentons le diagramme indiquant la position des doigts sur le manche au dessus de la portée et la tablature en dessous. Ici, c'est au total six notes qui sont jouées : ''mi'' grave, ''do'' médium, ''mi'' médium, ''sol'' médium, ''do'' aigu, ''mi'' aigu. Mais il s'agit bien des trois notes ''do'', ''mi'' et ''sol'' jouées à des octaves différentes. Nous remarquons également que la note de basse (la note la plus grave), ''mi'', est différente de la note fondamentale (celle qui donne le nom à l'accord), ''do'' ; l'accord est dit « renversé » (voir plus loin). === Comment joue-t-on un accord ? === Les notes ne sont pas forcément jouées en même temps ; elles peuvent être « égrainées », jouée successivement, ce que l'on appelle un arpège. La partition ci-dessous montre six manières différentes de jouer un accord de ''la'' mineur à la guitare, plaqué puis arpégé. [[Fichier:La mineur differentes executions.svg|class=transparent|center|Différentes exécution de l'accord de do majeur à la guitare.]] [[Fichier:La mineur differentes executions midi.midi|vignette|Différentes exécution de l'accord de la mineur à la guitare.]] Vous pouvez écouter l'exécution de cette partition avec le lecteur ci-contre. Seuls les instruments polyphoniques peuvent jouer les accords plaqués : instruments à clavier (clavecin, orgue, piano, accordéon), les instruments à plusieurs cordes pincées (harpe, guitare ; violon, alto, violoncelle et contrebasse joués en pizzicati). Les instruments à corde frottés de la famille du violon peuvent jouer des notes par deux à l'archet mais pas plus du fait de la forme bombée du chevalet ; cependant, un mouvement rapide permet de jouer les quatre cordes de manière très rapprochée. Les instruments à percussion de type xylophone ou le tympanon permettent de jouer jusqu'à quatre notes simultanément en tenant deux baguettes (mailloches, maillets) par main. Tous les instruments peuvent jouer des arpèges même si, dans le cas des instruments monodiques, les notes ne continuent pas à sonner lorsque l'on passe à la note suivante. L'arpège peut être joué par l'instrument de basse (basson, violoncelle, contrebasse, guitare basse, pédalier de l'orgue…), notamment dans le cas d'une basse continue ou d'une ''{{lang|en|walking bass}}'' (« basse marchante » : la basse joue des noires, donnant ainsi l'impression qu'elle marche). En jazz, et spécifiquement au piano, on a recours au ''{{lang|en|voicing}}'' : on choisit la manière dont on organise les notes pour donner une couleur spécifique, ou bien pour créer une mélodie en enchaînant les accords. Il est fréquent de ne pas jouer toutes les notes : si on n'en garde que deux, ce sont la tierce et la septième, car ce sont celles qui caractérisent l'accord (selon que la tierce est mineure ou majeure, que la septième est majeure ou mineure), et la fondamentale est en général jouée par la contrebasse ou guitare basse. {{clear}} === Classes d'accord === [[Fichier:Intervalles harmoniques accords classes.svg|vignette|upright=1.5|Intervalles harmoniques dans les accords classés de trois, quatre et cinq notes.]] Un accord composé d'empilement de tierces est appelé « accord classé ». En musique tonale, c'est-à-dire la musique fondée sur les gammes majeures ou mineures (cas majoritaire en musique classique), on distingue trois classes d'accords : * les accords de trois notes, ou triades, ou accords de quinte ; * les accords de quatre notes, ou accords de septième ; * les accords de cinq notes, ou accords de neuvième. En empilant des tierces, si l'on part de la note fondamentale, on a donc de intervalles de tierce, quinte, septième et neuvième. En musique tonale, les accords avec d'autres intervalles (hors renversement, voir ci-après), typiquement seconde, quarte ou sixte, sont considérés comme des transitions entre deux accords classés. Ils sont appelés, selon leur utilisation, « accords à retard » (en anglais : ''{{lang|en|suspended chord}}'', accord suspendu) ou « appoggiature » (note « appuyée », étrangère à l'harmonie). Voir aussi plus loin la notion de note étrangère. === Renversements d'accords === [[File:Accord do majeur renversements.svg|thumb|Accord parfait de do majeur et ses renversements.]] [[Fichier:Progression dominante renverse parfait do majeur.svg|vignette|upright=0.6|Progression accord de dominante renversé → accord parfait en ''do'' majeur.]] Un accord classé est donc un empilement de tierces. Si l'on change l'ordre des notes, on a toujours le même accord mais il est fait avec d'autres intervalles harmoniques. Par exemple, l'accord parfait de ''do'' majeur dans son état fondamental, c'est-à-dire non renversé, s'écrit ''do'' - ''mi'' - ''sol''. Sa note fondamentale, ''do'', est aussi se note de basse. Si maintenant on prend le ''do'' de l'octave supérieure, l'accord devient ''mi - sol - do'' ; c'est l'empilement d'une tierce ''(mi - sol)'' et d'une quarte ''(sol - do)'', soit la superposition d'une tierce ''(mi - sol)'' et d'une sixième ''(mi - do)''. C'est le premier renversement de l'accord parfait de ''do'' majeur ; la fondamentale est toujours ''do'' mais la basse est ''mi''. Le second renversement est ''sol - do - mi''. L'utilisation de renversement peut faciliter l'exécution de la progression d'accord. Par exemple, en tonalité ''do'' majeur, si l'on veut passer de l'accord de dominante ''sol - si - ré'' à l'accord parfait ''do - mi - sol'', alors on peut utiliser le second renversement de l'accord de dominante : ''ré - sol - si'' → ''do - mi - sol''. Ainsi, la basse descend juste d'un ton ''(ré → do)'' et sur un piano, la main reste globalement dans la même position. Le renversement d'un accord permet également de respecter certaines règles de l'harmonie classique, notamment éviter que des voix se suivent strictement (« mouvement parallèle »), ce qui aurait un effet de platitude. De manière générale, la notion de renversement permet deux choses : * d'enrichir l'œuvre : pour créer une harmonie donnée (c'est-à-dire des sons sonnant bien ensemble), nous avons plus de souplesse, nous pouvons organiser ces notes comme nous le voulons selon les voix ; * de simplifier l'analyse : quelle que soit la manière dont sont organisées les notes, cela nous ramène à un même accord. {{citation bloc|Or il, y a plusieurs manières de jouer un accord, selon que l'on aborde par la première note qui le constitue, ''do mi sol'', la deuxième, ''mi sol do'', ou la troisième note, ''sol do mi''. Ce sont les renversements, [que Rameau] va classer en différentes combinaisons d'une seule matrice. Faisant cela, Rameau divise le nombre d'accords [de septième] par quatre. Il simplifie, il structure […].|{{ouvrage|prénom1=André |nom1=Manoukian |titre=Sur les routes de la musique |éditeur=Harper Collins |année=2021 |passage=54 |isbn=979-1-03391201-9}} }} {{clear}} [[File:Plusieurs realisation 1er renversement doM.svg|thumb|Plusieurs réalisation du premier renversement de l'accord de ''do'' majeur.]] Notez que # Les notes peuvent être répétées plusieurs fois, à différentes octaves. # Dans la notion de renversement, seule importe en fait la note de basse. Ainsi, les accords ''mi-sol-do'', ''mi-do-sol'', ''mi-do-mi-sol'', ''mi-sol-mi-do''… sont tous une déclinaison du premier renversement de ''do-mi-sol'' et ils seront abrégés de la même manière (''mi''<sup>6</sup> en musique classique ou C/E en musique populaire et jazz, voir plus bas). {{clear}} == Notation des accords de trois notes == Les accords de trois notes sont appelés « accords de quinte » en classique, et « triades » en jazz. [[Fichier:Progression dominante renverse parfait do majeur chiffrage.svg|vignette|upright=0.7|Chiffrage du second renversement d'un accord de ''sol'' majeur et d'un accord de ''do'' majeur : notation en musique populaire et jazz (haut) et notation de basse chiffrée (bas).]] Les accords sont construits de manière systématique. Nous pouvons donc les représenter de manière simplifiée. Cette notation simplifiée des accords est appelée « chiffrage ». Reprenons la progression d'accords ci-dessus : « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » dans la tonalité de ''do'' majeur. On utilise en général trois notations différentes : * en musique populaire, jazz, rock… un accord est désigné par sa note fondamentale ; ici donc, les accords sont notés « ''sol'' - ''do'' » ou, en notation anglo-saxonne, « G - C » ;<br /> comme le premier accord est renversé, on indique la note de basse après une barre, la progression d'accords est donc chiffrée '''« ''sol''/''ré'' - ''do'' »''' ou '''« G/D - C »''' ;<br /> il s'agit ici d'accords composés d'une tierce majeure et d'une quinte juste ; si les accords sont constitués d'intervalles différents, nous ajoutons un symbole après la note : « m » ou « – » si la tierce est mineure, « dim » ou « ° » si la quinte est diminuée ; * en musique classique, on utilise la notation de « basse chiffrée » (utilisée notamment pour noter la basse continue en musique baroque) : on indique la note de basse sur la portée et on lui adjoint l'intervalle de la fondamentale à la note la plus haute (donc ici respectivement 6 et 5, puisque ''sol''-''si'' est une sixte et ''do''-''sol'' est une quinte), étant sous-entendu que l'on a des empilements de tierce en dessous ; mais dans le cas du premier accord, le premier intervalle n'est pas une tierce, mais une quarte ''(ré''-''sol)'', on note donc '''« ''ré'' <sup>6</sup><sub>4</sub> - ''do'' <sup>5</sup> »'''<ref>quand on ne dispose pas de la notation en supérieur (exposant) et inférieur (indice), on utilise parfois une notation sous forme de fraction : ''sol'' 6/4 et ''do'' 5/.</ref> ; * lorsque l'on fait l'analyse d'un morceau, on s'attache à identifier la note fondamentale de l'accord (qui est différente de la basse dans le cas d'un renversement) ; on indique alors le degré de la fondamentale : '''« {{Times New Roman|V<sup>6</sup><sub>4</sub> - I<sup>5</sup>}} »'''. La notation de basse chiffrée permet de construire l'accord à la volée : * on joue la note indiquée (basse) ; * s'il n'y a pas de 2 ni de 4, on lui ajoute la tierce ; * on ajoute les intervalles indiqués par le chiffrage. La notation de musique jazz oblige à connaître la composition des différents accords, mais une fois que ceux-ci sont acquis, il n'y a pas besoin de reconstruire l'accord. La notation de basse chiffrée avec les chiffres romains n'est pas utilisée pour jouer, mais uniquement pour analyser ; Sur les partitions avec basse chiffrée, il y a simplement les chiffrages indiqués au-dessus de la partie de basse. Le chiffrage avec le degré en chiffres romains présente l'avantage d'être indépendant de la tonalité et donc de se concentrer sur la fonction de l'accord au sein de la tonalité. Par exemple, ci-dessous, nous pouvons parler de la progression d'accords « {{Times New Roman|V - I}} » de manière générale, cette notation étant valable quelle que soit la tonalité. [[File:Progression dominante renverse parfait do majeur chiffrage basse continue.svg|thumb|Chiffrage en notation basse chiffrée de la progression d'accords « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » en do majeur.]] {{note|En notation de base continue avec fondamentale en chiffres romains, la fondamentale est toujours indiquée ''sous'' la portée de la partie de basse. Les intervalles sont indiqués au-dessus de la portée de la partie de basse ; lorsque l'on fait une analyse, on peut ayssi les indiquer à côté du degré en chiffres romains, donc sous la portée de la basse.}} {{note|En notation rock, le 5 en exposant indique un accord incomplet avec uniquement la fondamentale et la quinte, un accord sans tierce appelé « accord de puissance » ou ''{{lang|en|power chord}}''. Par exemple, C<sup>5</sup> est l'accord ''do-sol''.}} {{clear}} [[Fichier:Accords parfait do majeur basse chiffree fondamental et renverse.svg|vignette|upright=2.5|Chiffrage de l'accord parfait de ''do'' majeur en basse chiffrée, à l'état fondamental et ses renversements.]] Concernant les accords parfaits en notation de basse chiffrée : * un accord parfait à l'état fondamental est chiffré « <sup>5</sup> » ; on l'appelle « accord de quinte » ; * le premier renversement est chiffré « <sup>6</sup> » (la tierce est implicite) ; on l'appelle « accord de sixte » ; * le second renversement est noté « <sup>6</sup><sub>4</sub> » ; on l'appelle « accord de sixte et de quarte » (ou bien « de quarte et de sixte »). Par exemple, pour l'accord parfait de ''do'' majeur : * l'état fondamental ''do''-''mi''-''sol'' est noté ''do''<sup>5</sup> ; * le premier renversement ''mi''-''sol''-''do'' est noté ''mi''<sup>6</sup> ; * le second renversement ''sol''-''do''-''mi'' est noté ''sol''<sup>6</sup><sub>4</sub>. Il y a une exception : l'accord construit sur la sensible (7{{e}} degré) contient une quinte diminuée et non une quinte juste. Le chiffrage est donc différent : * l'état fondamental ''si''-''ré''-''fa'' est noté ''si''<sup><s>5</s></sup> (cinq barré), « accord de quinte diminuée » ; * le premier renversement ''ré''-''fa''-''si'' est noté ''ré''<sup>+6</sup><sub>3</sub>, « accord de sixte sensible et tierce » ; * le second renversement ''fa''-''si''-''ré'' est noté ''fa''<sup>6</sup><sub>+4</sub>, « accord de sixte et quarte sensible ». Par ailleurs, on ne considère pas qu'il est fondé sur la sensible, mais sur la dominante. Si certains indiquent le degré {{Times New Roman|VII}}, d'autres indiquent le degré {{Times New Roman|V}} en mettant des guillemets, « {{Times New Roman|“V”}} ». Donc selon l'état, le chiffrage est {{Times New Roman|“V”<sup><s>5</s></sup>}}, {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}} ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}. En notation jazz, on ajoute « dim », « <sup>o</sup> » ou bien « <sup>♭5</sup> » au chiffrage, ici : B dim, B<sup>o</sup> ou B<sup>♭5</sup> pour l'état fondamental. Pour les renversements : B dim/D et B dim/F ; ou bien B<sup>o</sup>/D et B<sup>o</sup>/F ; ou bien B<sup>♭5</sup>/D et B<sup>♭5</sup>/F. {{clear}} [[Fichier:Accords basse chiffree basse do fondamental et renverses.svg|vignette|upright=2|Basse chiffrée : accords de quinte, de sixte et de sixte et de quarte ayant pour basse ''do''.]] Et concernant les accords ayant pour basse ''do'' en tonalité de ''do'' majeur : * l'accord ''do''<sup>5</sup> est un accord à l'état fondamental, c'est donc l'accord ''do''-''mi''-''sol'' (sa fondamentale est ''do'') ; * l'accord ''do''<sup>6</sup> est le premier renversement d'un accord, c'est donc l'accord ''do''-''mi''-''la'' (sa fondamentale est ''la'') ; * l'accord ''do''<sup>6</sup><sub>4</sub> est le second renversement d'un accord, c'est donc l'accord ''do''-''fa''-''la'' (sa fondamentale est ''fa''). {{clear}} == Notes étrangères == La musique européenne s'appuie essentiellement sur des accords parfaits, c'est-à-dire fondés sur une tierce majeure ou mineure, et une quinte juste. Il arrive fréquemment qu'un accord ne soit pas un accord parfait. Les notes qui font partie de l'accord parfait sont appelées « notes naturelles » et la note qui n'en fait pas partie est appelée « note étrangère ». Il existe plusieurs types de notes étrangères : * anticipation : la note étrangère est une note naturelle de l'accord suivant ; * appogiature : note d'ornementation qui se résout par mouvement conjoint, c'est-à-dire qu'elle est suivie par une note située juste au-dessus ou en dessous (seconde ascendante ou descendante) qui est, elle, une note naturelle ; * broderie : on part d'une note naturelle, on monte ou on descend d'une seconde, puis on revient sur la note naturelle ; * double broderie : on part d'une note naturelle, on joue la note du dessus puis la note du dessous avant de revenir à la note naturelle ; ou bien on joue la note du dessous puis la note du dessus ; * échappée : note étrangère n'appartenant à aucune des autres catégories ; * note de passage : mouvement conjoint allant d'une note naturelle d'un accord à une note naturelle de l'accord suivant ; * pédale : la note de basse reste la même pendant plusieurs accords successifs ; * retard : la note étrangère est une note naturelle de l'accord précédent. Les notes étrangères ne sont pas chiffrées. [[File:Notes etrangeres accords.svg|center|Différents types de notes étrangères.]] {{note|Les anglophones distinguent deux types de retard : la ''{{lang|en|suspension}}'' est résolue vers le haut (le mouvement est ascendant), le ''{{lang|en|retardation}}'' est résolu vers le bas (le mouvement est descendant).}} == Principaux accords == Les trois principaux accords sont : * l'accord parfait majeur : il est construit sur les degrés {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (médiante) et {{Times New Roman|V}} (dominante) d'une gamme majeure ; il est noté {{Times New Roman|I}}<sup>5</sup>, {{Times New Roman|IV}}<sup>5</sup>, {{Times New Roman|V}}<sup>5</sup> ; * l'accord parfait mineur : il est construit sur les degrés {{Times New Roman|I}} (tonique) et {{Times New Roman|IV}} (sous-tonique) d'une gamme mineure harmonique ; il est également noté {{Times New Roman|I}}<sup>5</sup> et {{Times New Roman|IV}}<sup>5</sup>, les anglo-saxons le notent {{Times New Roman|i}}<sup>5</sup> et {{Times New Roman|iv}}<sup>5</sup> (la minuscule indiquant le caractère mineur) ; * l'accord de septième de dominante : il est construit sur le degré {{Times New Roman|V}} (dominante) d'une gamme majeure ou mineure harmonique ; il est noté {{Times New Roman|V}}<sup>7</sup><sub>+</sub>. On peut trouver ces trois accords sur d'autres degrés, et il existe d'autre types d'accords. Nous verrons cela plus loin. {| class="wikitable" |+ Constitution des principaux accords — Dénomination classique |- ! scope="col" | Accord ! scope="col" | 1<sup>er</sup> intervalle ! scope="col" | 2<sup>e</sup> intervalle ! scope="col" | 3<sup>e</sup> intervalle |- ! scope="row" | Accord parfait majeur | tierce majeure (3M) || quinte juste (5J) || — |- ! scope="row" | Accord parfait mineur | tierce mineure (3m) || quinte juste (5J) || — |- ! scope="row" | Accord de septième de dominante | tierce majeure (3M) || quinte juste (5J) || septième mineure (7m) |} {| class="wikitable" |+ Constitution des principaux accords — Dénomination jazz |- ! scope="col" | Accord ! scope="col" | 1<sup>er</sup> intervalle ! scope="col" | 2<sup>e</sup> intervalle ! scope="col" | 3<sup>e</sup> intervalle |- ! scope="row" | Triade majeure | tierce majeure (3M) || quinte juste (5J) || — |- ! scope="row" | Triade mineure | tierce mineure (3m) || quinte juste (5J) || — |- ! scope="row" | Accord de septième | tierce majeure (3M) || quinte juste (5J) || septième mineure (7m) |} {| border="0" |- | [[Fichier:Accord do majeur arpege puis plaque.midi | Accord parfait de ''do'' majeur (C).]] || [[Fichier:Accord do mineur arpege puis plaque.midi | Accord parfait de ''do'' mineur (Cm).]] || [[Fichier:Accord do septieme arpege puis plaque.midi | Accord de septième de dominante de ''fa'' majeur (C<sup>7</sup>).]] |- | Accord parfait<br /> de ''do'' majeur (C). || Accord parfait<br /> de ''do'' mineur (Cm). || Accord de septième de dominante<br /> de ''fa'' majeur (C<sup>7</sup>). |} '''Rappel :''' * la tierce mineure est composée d'un ton et demi (1 t ½) ; * la tierce majeur est composée de deux tons (2 t) ; * la quinte juste a la même altération que la fondamentale, sauf lorsque la fondamentale est ''si'' (la quinte juste est alors ''fa''♯) ; * la septième mineure est le renversement de la seconde majeure (1 t). [[File:Renversements accords pft fa maj basse chiffree.svg|thumb|Renversements de l'accord parfait de ''fa'' majeur, et la notation de basse chiffrée.]] [[File:Renversements accord sept de dom fa maj basse chiffree.svg|thumb|Renversements de l'accord de septième de dominante de ''fa'' majeur, et la notation de basse chiffrée.]] {| class="wikitable" |+ Notation des principaux accords en musique classique |- ! scope="col" | Accord ! scope="col" | État<br /> fondamental ! scope="col" | Premier<br /> renversement ! scope="col" | Deuxième<br /> renversement ! scope="col" | Troisième<br /> renversement |- ! scope="row" | Accord parfait | {{Times New Roman|I<sup>5</sup>}}<br/> acc. de quinte || {{Times New Roman|I<sup>6</sup>}}<br :> acc. de sixte || {{Times New Roman|I<sup>6</sup><sub>4</sub>}}<br /> acc. de quarte et de sixte || — |- ! scope="row" | Accord de septième<br /> de dominante | {{Times New Roman|V<sup>7</sup><sub>+</sub>}}<br /> acc.de septième de dominante || {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}<br />acc. de sixte et quinte diminuée || {{Times New Roman|V<sup>+6</sup>}}<br />acc. de sixte sensible || {{Times New Roman|V<sup>+4</sup>}}<br />acc. de quarte sensible<br />acc. de triton |} {| class="wikitable" |+ Notation des principaux accords en jazz |- ! scope="col" | Accord ! scope="col" | Chiffrage ! scope="col" | Renversements |- ! scope="row" | Triade majeure | X | rowspan="3" | Les renversements se notent en mettant la basse après une barre de fraction, par exemple pour la triade de ''do'' majeur : * état fondamental : C ; * premier renversement : C/E ; * second renversement : C/G. |- ! scope="row" | Triade mineure | Xm, X– |- ! scope="row" | Septième | X<sup>7</sup> |} {{clear}} Dans le cas d'un accord de septième de dominante, le nom de l'accord change selon que l'on est en musique classique ou en jazz : en musique classique, on donne le nom de la tonalité alors qu'en jazz, on donne le nom de la fondamentale. Ainsi, l'accord appelé « septième de dominante de ''do'' majeur » en musique classique, est appelé « ''sol'' sept » (G<sup>7</sup>) en jazz : la dominante (degré {{Times New Roman|V}}, dominante) de la tonalité de ''do'' majeur est la note ''sol''. Comment appelle-t-on en musique classique l'accord appelé « ''do'' sept » (C<sup>7</sup>) en jazz ? Les tonalités dont le ''do'' est la dominante sont les tonalités de ''fa'' majeur (''si''♭ à la clef) et de ''fa'' mineur harmonique (''si''♭, ''mi''♭, ''la''♭ et ''ré''♭ à la clef et ''mi''♮ accidentel). Il s'agit donc de l'accord de septième de dominante des tonalités de ''fa'' majeur et ''fa'' mineur harmonique. {| class="wikitable" |+ Accords fréquents pour quelques la tonalités majeures ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Accord parfait<br />{{Times New Roman|I<sup>5</sup>}} ! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}} |- |''Do'' majeur || || C<br />''do-mi-sol'' || G7<br />''sol-si-ré-fa'' |- |''Sol'' majeur || ''fa''♯ || G<br />''sol-si-ré'' || D7<br />''ré-fa''♯''-la-do'' |- |''Ré'' majeur || ''fa''♯, ''do''♯ || D<br />''ré-fa''♯''-la'' || A7<br />''la-do''♯''-mi-sol'' |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A<br />''la-do''♯''-mi'' || E7<br />''mi-sol''♯''-si-ré'' |- | ''Fa'' majeur || ''si''♭ || F<br />''fa-la-do'' || C7<br />''do-mi-sol-si''♭ |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭<br />''si''♭''-ré-fa'' || F7<br />''fa-la-do-mi''♭ |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭<br />''mi''♭''-sol-si''♭ || B♭7<br />''si''♭''-ré-fa-la''♭ |} {| class="wikitable" |+ Accords fréquents pour quelques la tonalités mineures harmoniques ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Accord parfait<br />{{Times New Roman|i<sup>5</sup>}} ! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}} |- |''La'' mineur<br />harmonique || || Am, A–<br />''la-do-mi'' || E7<br />''mi-sol''♯''-si-ré'' |- |''Mi'' mineur<br />harmonique || ''fa''♯ || Em, E–<br />''mi-sol-si'' || B7<br />''si-ré''♯''-fa''♯''-la'' |- |''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || Bm, B–<br />''si-ré-fa''♯ || F♯7<br />''fa''♯''la''♯''-do''♯''-mi'' |- |''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || F♯m, F♯–<br />''fa''♯''-la-do''♯ || C♯7<br />''do''♯''-mi''♯''-sol''♯''-si'' |- | ''Ré'' mineur<br />harmonique || ''si''♭ || Dm, D–<br />''ré-fa-la'' || A7<br />''la-do''♯''-mi-sol'' |- | ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || Gm, G–<br />''sol-si''♭''-ré'' || D7<br />''ré-fa''♯''-la-do'' |- | ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || Cm, C–<br />''do-mi''♭''-sol'' || G7<br />''sol-si''♮''-ré-fa'' |} {{clear}} == Accords sur les degrés d'une gamme == === Harmonisation d'une gamme === [[Fichier:Accord trois notes gamme do majeur chiffre.svg|vignette|upright=1.2|Accords de trois note sur la gamme de ''do'' majeur, chiffrés.]] On peut ainsi construire une triade par degré d'une gamme. Pour une gamme majeure, les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|V<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|II<sup>5</sup>}}, {{Times New Roman|III<sup>5</sup>}}, {{Times New Roman|VI<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|ii<sup>5</sup>}}, {{Times New Roman|iii<sup>5</sup>}}, {{Times New Roman|vi<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}. Les accords ont tous une quinte juste à l'exception de l'accord {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} qui a une quinte diminuée, raison pour laquelle le « 5 » est barré. C'est un accord dit « de quinte diminuée ». En jazz, l'accord diminué est noté « dim », « ° », « m<sup>♭5</sup> » ou « <sup>–♭5</sup> ». Nous avons donc trois types d'accords (dans la notation jazz) : X (triade majeure), Xm (triade mineure) et X° (triade diminuée), la lettre X remplaçant le nom de la note fondamentale. {{clear}} [[Fichier:Accord trois notes gamme la mineur chiffre.svg|vignette|upright=1.2|Accords de trois notes sur une gamme de ''la'' mineur harmonique, chiffrés.]] Pour une gamme mineure harmonique, les accords {{Times New Roman|III<sup>+5</sup>}}, {{Times New Roman|V<sup>♯</sup>}} et {{Times New Roman|VI<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|II<sup><s>5</s></sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|i<sup>5</sup>}}, {{Times New Roman|ii<sup><s>5</s></sup>}}, {{Times New Roman|iv<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}. Les accords {{Times New Roman|ii<sup><s>5</s></sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} ont une quinte diminuée ; ce sont des accords dits « de quinte diminuée ». L'accord {{Times New Roman|III<sup>+5</sup>}} a une quinte augmentée ; le signe « plus » indique que la note de cinquième, le ''sol'' dièse, est la sensible. En jazz, l'accord est noté « aug » ou « <sup>+</sup> ». Les autres accords ont une quinte juste. Aux trois accords générés par une gamme majeure (X, Xm et X°), nous voyons ici apparaître un quatrième type d'accord : la triade augmentée X<sup>+</sup>. Nous remarquons que des gammes ont des accords communs. Par exemple, l'accord {{Times New Roman|ii<sup>5</sup>}} de ''do'' majeur est identique à l'accord {{Times New Roman|iv<sup>5</sup>}} de ''la'' mineur (il s'agit de l'accord Dm). Quel que soit le mode, les accords construits sur la sensible (accord de quinte diminuée) sont rarement utilisés. S'ils le sont, c'est en tant qu'accord de septième de dominante sans fondamentale (voir ci-après). C'est la raison pour laquelle le chiffrage indique le degré {{Times New Roman|V}} entre guillemets, et non pas le degré {{Times New Roman|VII}} (mais pour des raisons de clarté, nous l'indiquons entre parenthèses au début). En mode mineur, l'accord de quinte augmentée {{Times New Roman|iii<sup>+5</sup>}} est très peu utilisé (voir plus loin ''[[#Progression_d'accords|Progression d'accords]]''). C'est un accord considéré comme dissonant. On voit que : * un accord parfait majeur peut appartenir à cinq gammes différentes ;<br /> par exemple l'accord parfait de ''do'' majeur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> degré de la gamme de ''do'' majeur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' mineur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''mi'' mineur ; * un accord parfait mineur peut appartenir à cinq gammes différentes ;<br />par exemple l'accord parfait de ''la'' mineur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> de la gamme de ''la'' mineur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''mi'' mineur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|III}}<sup>e</sup> degré de ''fa'' majeur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''do'' majeur ; * un accord de quinte diminuée peut appartenir à trois gammes différentes ;<br />par exemple, l'accord de quinte diminuée de ''si'' est l'accord construit sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' majeur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''la'' mineur et sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' mineur ; * un accord de quinte augmentée (à l'état fondamental) ne peut appartenir qu'à une seule gamme ;<br /> par exemple, l'accord de quinte augmentée de ''do'' est l'accord construit sur le {{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur. {| class="wikitable" |+ Notation jazz des triades |- | rowspan="2" colspan="2" | ! scope="col" colspan="2" | Tierce |- ! scope="col" | 3m ! scope="col" | 3M |- ! rowspan="3" | Quinte ! scope="row" | 5d | Xᵒ, X<sub>m</sub><sup>(♭5)</sup> || |- ! scope="row" | 5J | Xm, X– || X |- ! scope="row" | 5A | || X+, X<sup>(♯5)</sup> |} === Harmonisation par des accords de septième === [[Fichier:Harmonisation gamme do majeur par septiemes chiffre.svg|vignette|upright=2|Harmonisation de la gamme de do majeur par des accords de septième.]] Les accords de septième contiennent une dissonance et créent ainsi une tension. Ils sont très utilisés en jazz. Nous avons représenté ci-contre l'harmonisation de la gamme de ''do'' majeur. La constitution des accords est la suivantes : * tierce majeure (3M) ** quinte juste (5J) *** septième mineure (7m) : sur le degré V, c'est l'accord de septième de dominante V<sup>7</sup><sub>+</sub>, noté X<sup>7</sup> (X pour G), *** septième majeure (7M) : sur les degrés I et IV, appelés « accords de septième majeure » et notés aussi X<sup>maj7</sup> ou X<sup>Δ</sup> (X pour C ou F) ; * tierce mineure (3m) ** quinte juste (5J) *** septième mineure : sur les degrés ii, iii et vi, appelés « accords mineur septième » et notés Xm<sup>7</sup> ou X–<sup>7</sup> (X pour D, E ou A), ** quinte diminuée (5d) *** septième mineure (7m) : sur le degré vii, appelé « accord demi-diminué » (puisque seule la quinte est diminuée) et noté X<sup>∅</sup> ou Xm<sup>7(♭5)</sup> ou X–<sup>7(♭5)</sup> (X pour B) ;<br /> en musique classique, on considère que c'est un accord de neuvième de dominante sans fondamentale. Nous avons donc quatre types d'accords : X<sup>7</sup>, X<sup>maj7</sup>, Xm<sup>7</sup> et X<sup>∅</sup> En jazz, on ajoute souvent la quarte à l'accord de sous-dominante IV (sur le ''fa'' dans une gamme de ''do'' majeur) ; il s'agit ici d'une quarte augmentée (''fa''-''si'') et l'accord est surnommé « accord lydien » mais cette dénomination est erronée (il s'agit d'une mauvaise interprétation de textes antiques). C'est un accord de onzième sans neuvième (la onzième étant l'octave de la quarte), il est noté X<sup>maj7(♯11)</sup> ou X<sup>Δ(♯11)</sup> (ici, F<sup>maj7(♯11)</sup>, ''fa''-''la''-''do''-''mi''-''si'' ou ''fa''-''la''-''si''-''do''-''mi''). {| class="wikitable" |+ Chiffrage jazz des accords de septième |- ! scope="col" rowspan="2" | Tierce ! scope="col" rowspan="2" | Quinte ! scope="col" colspan="2" | Septième |- ! scope="col" | 7m ! scope="col" | 7M |- | rowspan="2" | 3m | 5d || X<sup>∅</sup>, X<sub>m</sub><sup>7(♭5)</sup>, X–<sup>7(♭5)</sup> || |- | rowspan="2" | 5J | X<sub>m</sub><sup>7</sup>, X–<sup>7</sup> || |- | rowspan="2" | 3M | X<sup>7</sup> || X<sup>maj7</sup>, X<sup>Δ</sup> |- | 5A || || X<sub>+</sub><sup>maj7</sup>, X<sub>+</sub><sup>Δ</sup> |} === Modulation et emprunt === Un morceau peut comporter des changements de tonalité; appelés « modulation ». Il y a parfois un court passage dans une tonalité différente, typiquement sur une ou deux mesures, avant de retourner dans la tonalité d'origine : on parle d'emprunt. Lorsqu'il y a une modulation ou un emprunt, les degrés changent. Un même accord peut donc avoir une fonction dans une partie du morceau et une autre fonction ailleurs. L'utilisation d'accord différents, et en particulier d'accord utilisant des altérations accidentelles, indique clairement une modulation. Nous avons vu précédemment que les modulations courantes sont : * les modulations dans les tons voisins ; * les modulations homonymes ; * les marches harmoniques. Une modulation entre une tonalité majeure et mineure change la couleur du passage, * la modulation la plus « douce » est entre les tonalités relatives (par exemple''do'' majeur et ''la'' mineur) car ces tonalités utilisent quasiment les mêmes notes ; * la modulation la plus « voyante » est la modulation homonyme (par exemple entre ''do'' majeur et ''do'' mineur). Une modulation commence souvent sur l'accord de dominante de la nouvelle tonalité. Pour analyser un œuvre, ou pour improviser sur une partie, il est important de reconnaître les modulations. La description de la successind es tonalités s'appelle le « parcours tonal ». === Exercices élémentaires === L'apprentissage des accords passe par quelques exercices élémentaires. '''1. Lire un accord''' Il s'agit de lecture de notes : des notes composant les accords sont écrites « empilées » sur une portée, il faut les lire en énonçant les notes de bas en haut. '''2. Reconnaître la « couleur » d'un accord''' On écoute une triade et il faut dire si c'est une triade majeure ou mineure. Puis, on complexifie l'exercice en ajoutant la septième. '''3. Chiffrage un accord''' Trouver le nom d'un accord à partir des notes qui le composent. '''4. Réalisation d'un accord''' Trouver les notes qui composent un accord à partir de son nom. '''5. Dictée d'accords''' On écoute une succession d'accords et il faut soit écrire les notes sur une portée, soit écrire les noms de accords. [[File:Exercice constitution accord basse chiffree.svg|thumb|Exercice : constitution d'accord à partir de la basse chiffrée.]] '''Exercices de basse chiffrée''' ''Réalisation d'un accord'' Sur la figure suivante, écrire les notes des accords correspondant à la basse chiffrée. Déterminer le degré de la fondamentale pour chaque accord en considérant que nous sommes dans la tonalité de ''sol'' majeur. {{boîte déroulante/début|titre=Solution}} [[File:Exercice constitution accord basse chiffree solution.svg|vignette|Solution.]] # La note de basse est un ''do''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''mi'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''sol''.<br />Le chiffrage « <sup>5</sup> » indique que c'est un accord dans son état fondamental (l'écart entre deux notes consécutives ne dépasse pas la tierce), la fondamentale est donc la basse, ''do'', qui est le degré IV de la tonalité. # La note de basse est un ''si''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''ré'', puis nous appliquons le chiffrage 6 et ajoutons la sixte, ''sol''.<br />Le chiffrage « <sup>6</sup> » indique que c'est un accord dans son premier renversement. En le remettant dans son état fondamental, nous obtenons ''sol-si-ré'', la fondamentale est donc la tonique, le degré I. # La note de basse est un ''la''. Nous ajoutons la tierce (chiffre 3), ''do'', et la sixte (6), ''fa''♯. Nous vérifions que le ''fa''♯ est la sensible (signe +)<br />Nous voyons un « blanc » entre les notes ''do'' et ''fa''♯. En descendant le ''fa''♯ à l'octave inférieure, nous obtenons un empilement de tierces ''fa''♯-''la-do'', le fondamentale est donc ''fa''♯, le degré VII. Nous pouvons le voir comme le deuxième renversement de l'accord de septième de dominante, sans fondamentale. # La note de basse est un ''fa''♯. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''la'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''do'' ; nous vérifions qu'il s'agit bien d'une quinte diminuée (le 5 est barré). Nous appliquons le chiffre 6 et ajoutons la sixte, ''ré''.<br />Nous voyons que les notes ''do'' et ''ré'' sont conjointes (intervalle de seconde). En descendant le ''ré'' à l'octave inférieure, nous obtenons un empilement de tierces ''ré-fa''♯-''la-do'', le fondamentale est donc ''ré'', le degré V. Nous constatons que l'accord chiffré est le premier renversement de l'accord de septième de dominante. {{boîte déroulante/fin}} {{clear}} ''Chiffrage d'accords'' [[Fichier:Exercice chiffrage accord basse chiffree.svg|vignette|Accords à chiffrer.]] Chiffrer les accords ci-contre. {{boîte déroulante/début|titre=Solution}} [[File:Exercice chiffrage accord basse chiffree solution.svg|vignette|Solution.]] # On relève les intervalles en partant de la basse : tierce majeure (3M) et quinte juste (5J). Le chiffrage complet est donc ''fa''<sup>5</sup><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''fa''<sup>5</sup>.<br /> On peut aussi reconnaître que c'est l'accord parfait sur la tonique de la tonalité de ''fa'' majeur dans son état fondamental, le chiffrage d'un accord parfait étant <sup>5</sup>. # On relève les intervalles en partant de la basse : quarte juste (4J), sixte majeure (6M). Le chiffrage complet est donc ''fa''<sup>6</sup><sub>4</sub>.<br /> On peut aussi reconnaître que c'est le second renversement de l'accord ''mi-sol-si'', sur la tonique de la tonalité de ''mi'' mineur, le chiffrage du second renversement d'un accord parfait étant <sup>6</sup><sub>4</sub>. # Les intervalles en partant de la basse sont : tierce mineure (3m), quinte diminuée (5d), sixte mineure (6m). Le chiffrage complet est donc ''mi''<sup>6</sup><small><s>5</s></small><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''mi''<sup>6</sup><sub><s>5</s></sub>.<br /> On reconnaît le premier renversement de l'accord ''do-mi-sol-si''♭, accord de septième de dominante de la tonalité de ''fa'' majeur. # Les intervalles en partant de la basse sont : tierce mineure (3m), quinte juste (5J), septième mineure (7m). Le chiffrage complet est donc ''ré''<sup>7</sup><small>5</small><sub>3</sub> ; c'est typique d'un accord de septième de dominante, son chiffrage est donc ''ré''<sup>7</sup><sub>+</sub>.<br /> On reconnaît l'accord de septième de dominante de la tonalité de ''sol'' mineur dans son état fondamental. {{boîte déroulante/fin}} {{clear}} [[File:Exercice constitution accord notation jazz.svg|thumb|Exercice : constitution d'un accord d'après son chiffrage en notation jazz.]] '''Exercices de notation jazz''' ''Réalisation d'un accord'' Sur la figure suivante, écrire les notes des accords correspondant aux chiffrages. {{boîte déroulante/début|titre=Solution}} [[File:Exercice constitution accord notation jazz solution.svg|thumb|Solution.]] # Il s'agit de la triade majeure de ''do'' dans son état fondamental. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''do-mi-sol''. # Il s'agit de la triade majeure de ''sol''. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''sol-si-ré''. On renverse l'accord afin que la basse soit le ''si'', l'accord est donc ''si-ré-sol''. # Il s'agit de l'accord demi-diminué de ''fa''♯. Les intervalles sont la tierce mineure (3m), la quinte diminuée (5d) et la septième mineure (7m). Les notes sont donc ''fa''♯-''la-do-mi''. Nous renversons l'accord afin que la basse soit le ''la'', l'accord est donc ''a-do-mi-fa''♯. # Il s'agit de l'accord de septième de ''ré''. Les intervalles sont donc la tierce majeure (3M), la quinte juste (5J) et la septième mineure (7m). Les notes sont ''ré-fa''♯''-la-do''. Nous renversons l'accord afin que la basse soit le ''fa''♯, l'accord est donc ''fa''♯''-la-do-ré''. {{boîte déroulante/fin}} {{clear}} ''Chiffrage d'accords'' [[File:Exercice chiffrage accord notation jazz.svg|thumb|Accords à chiffrer.]] Chiffrer les accords ci-contre. {{boîte déroulante/début|titre=Solution}} [[File:Exercice chiffrage accord notation jazz solution.svg|thumb|Solution.]] # Les notes sont toutes sur des interlignes consécutifs, c'est donc un empilement de tierces ; l'accord est dans son état fondamental. Les intervalles sont une tierce majeure (''fa-la'' : 3M) et une quinte juste (''fa-do'' : 5J), c'est donc la triade majeure de ''fa''. Le chiffrage est F. # Il y a un blanc dans l'empilement des notes, c'est donc un accord renversé. En permutant les notes pour n'avoir que des tierces, on trouve l'accord ''mi-sol-si''. Les intervalles sont une tierce mineure (''mi-sol'' : 3m) et une quinte juste (''mi-si'' : 5J), c'est donc la triade mineure de ''mi'' avec un ''si'' à la basse. Le chiffrage est Em/B ou E–/B. # Il y deux notes conjointes, c'est donc un renversement. L'état fondamental de cet accord est ''do-mi-sol-si''♭. Les intervalles sont une tierce majeure (''do-mi'' : 3M), une quinte juste (''do-sol'' : 5J) et une septième mineure (''do-si''♭ : 7m). C'est donc l'accord de ''do'' septième avec un ''mi'' à la basse, chiffré C<sup>7</sup>/E. # Les notes sont toutes sur des interlignes consécutifs, l'accord est dans son état fondamental. Les intervalles sont la tierce mineure (''ré-fa'' : 3m), une quinte juste (''ré-la'' : 5J) et une septième mineure (''ré-do'' : 7m). C'est donc l'accord de ''ré'' mineur septième, chiffré Dm<sup>7</sup> ou D–<sup>7</sup>. {{boîte déroulante/fin}} {{clear}} == Harmonie fonctionnelle == Le choix des accords et de leur succession — la progression des accords — est un élément important d'un morceau, de sa composition. Le compositeur ou la compositrice a bien sûr une liberté totale, mais pour faire des choix, il faut comprendre les conséquences de ces choix, et donc ici, les effets produits par les accords et leur progression. Une des manières d'aborder le sujet est l'harmonie fonctionnelle. === Les trois fonctions des accords === En harmonie tonale, on considère que les accords ont une fonction. Il existe trois fonctions : * la fonction de tonique, {{Times New Roman|I}} ; * la fonction de sous-dominante, {{Times New Roman|IV}} ; * la fonction de dominante, {{Times New Roman|V}}. L'accord de tonique, {{Times New Roman|I}}, est l'accord « stable » de la tonalité par excellence. Il conclut en général les morceaux, et ouvre souvent les morceaux ; il revient fréquemment au cours du morceau. L'accord de dominante, {{Times New Roman|V}}, est un accord qui introduit une instabilité, une tension. En particulier, il contient la sensible (degré {{Times New Roman|VI}}), qui est une note « aspirée » vers la tonique. Cette tension, qui peut être renforcée par l'utilisation d'un accord de septième, est fréquemment résolue par un passage vers l'accord de tonique. Nous avons donc deux mouvements typiques : {{Times New Roman|I}} → {{Times New Roman|V}} (création d'une tension, d'une attente) et {{Times New Roman|V}} → {{Times New Roman|I}} (résolution d'une tension). Les accords de tonique et de dominante ont le cinquième degré en commun, cette note sert donc de pivot entre les deux accords. L'accord de sous-dominante, {{Times New Roman|IV}}, est un accord qui introduit lui aussi une tension, mais moins grande : il ne contient pas la sensible. Notons que s'il est une quarte au-dessus de la tonique, il est aussi une quinte en dessous d'elle ; il est symétrique de l'accord de dominante. Il a donc un rôle similaire à l'accord de dominante, mais atténué. L'accord de sous-dominante aspire soit vers l'accord de dominante, très proche, et l'on a alors une augmentation de la tension ; soit vers l'accord de tonique, un retour vers la stabilité (il a alors un rôle semblable à la dominante). Du fait de ces deux bifurcations possibles — augmentation de la tension ({{Times New Roman|IV}} → {{Times New Roman|V}}) ou retour à la stabilité ({{Times New Roman|IV}} → {{Times New Roman|I}}) —, l'utilisation de l'accord de sous-dominante introduit un certain flottement : si l'on peut facilement prédire l'accord qui suit un accord de dominante, on ne peut pas prédire ce qui suit un accord de sous-dominante. Notons que la composition ne consiste pas à suivre ces règles de manière stricte, ce qui conduirait à des morceaux stéréotypés et plats. Le plaisir d'écoute joue sur une alternance entre satisfaction d'une attente (respect des règles) et surprise (rompre les règles). === Accords remplissant ces fonctions === Les accords sur les autres degrés peuvent se ramener à une de ces trois fonctions : * {{Times New Roman|II}} : fonction de sous-dominante {{Times New Roman|IV}} ; * {{Times New Roman|III}} (très peu utilisé en mode mineur en raison de sa dissonance) et {{Times New Roman|VI}} : fonction de tonique {{Times New Roman|I}} ; * {{Times New Roman|VII}} : fonction de dominante {{Times New Roman|V}}. En effet, les accords étant des empilements de tierces, des accords situés à une tierce l'un de l'autre — {{Times New Roman|I}} ↔ {{Times New Roman|III}}, {{Times New Roman|II}} ↔ {{Times New Roman|IV}}, {{Times New Roman|V}} ↔ {{Times New Roman|VII}}, {{Times New Roman|VI}} ↔ {{Times New Roman|VIII}} ( = {{Times New Roman|I}}) — ont deux notes en commun. On retrouve le fait que l'accord sur le degré {{Times New Roman|VII}} est considéré comme un accord de dominante sans tonique. En mode mineur, l'accord sur le degré {{Times New Roman|III}} est évité, il n'a donc pas de fonction. {|class="wikitable" |+ Fonction des accords |- ! scope="col" | Fondamentale ! scope="col" | Fonction |- | {{Times New Roman|I}} || tonique |- | {{Times New Roman|II}} || sous-dominante faible |- | {{Times New Roman|III}} || tonique faible |- | {{Times New Roman|IV}} || sous-dominante |- | {{Times New Roman|V}} || dominante |- | {{Times New Roman|VI}} || tonique faible |- | {{Times New Roman|VII}} || dominante faible |} Par exemple en ''do'' majeur : * fonction de tonique : '''''do''<sup>5</sup> (C)''', ''mi''<sup>5</sup> (E–), ''la''<sup>5</sup> (A–) ; * fonction de sous-dominante : '''''fa''<sup>5</sup> (F)''', ''ré''<sup>5</sup> (D–) ; * fonction de dominante : '''''sol''<sup>5</sup> (G)''' ou ''sol''<sup>7</sup><sub>+</sub> (G<sup>7</sup>), ''si''<sup> <s>5</s></sup> (B<sup>o</sup>). En ''la'' mineur harmonique : * fonction de tonique : '''''la''<sup>5</sup> (A–)''', ''fa''<sup>5</sup> (F) [, rarement : ''do''<sup>+5</sup> (C<sup>+</sup>)] ; * fonction de sous-dominante : '''''ré''<sup>5</sup> (D–)''', ''si''<sup> <s>5</s></sup> (B<sup>o</sup>) ; * fonction de dominante : '''''mi''<sup>5</sup> (E)''' ou ''mi''<sup>7</sup><sub>+</sub> (E<sup>7</sup>), ''sol''♯<sup> <s>5</s></sup> (G♯<sup>o</sup>). Le fait d'utiliser des accords différents pour remplir une fonction permet d'enrichir l'harmonie, et de jouer sur l'équilibre entre satisfaction d'une attente (on respecte les règles sur les fonctions) et surprise (mais on n'utilise pas l'accord attendu). === Les dominantes secondaires === On utilise aussi des accords de septième dominante se fondant sur un autre degré que la dominante de la gamme ; on parle de « dominante secondaire ». Typiquement, avant un accord de septième de dominante, on utilise parfois un accord de dominante de dominante, dont le degré est alors noté « {{Times New Roman|V}} de {{Times New Roman|V}} » ou « {{Times New Roman|V}}/{{Times New Roman|V}} » ; la fondamentale est de l'accord est alors situé cinq degrés au-dessus de la dominante ({{Times New Roman|V}}), c'est donc le degré {{Times New Roman|IX}}, c'est-à-dire le degré {{Times New Roman|II}} de la tonalité en cours). Ou encore, on utilise un accord de dominante du degré {{Times New Roman|IV}} (« {{Times New Roman|V}} de {{Times New Roman|IV}} », la fondamentale est alors le degré {{Times New Roman|I}}) avant un accord sur le degré {{Times New Roman|IV}} lui-même. Par exemple, en tonalité de ''do'' majeur, on peut trouver un accord ''ré - fa''♯'' - la - do'' (chiffré {{Times New Roman|V}} de {{Times New Roman|V}}<sup>7</sup><sub>+</sub>), avant un accord ''sol - si - ré - fa'' ({{Times New Roman|V}}<sup>7</sup><sub>+</sub>). L'accord ''ré - fa''♯'' - la - do'' est l'accord de septième de dominante des tonalités de ''sol''. Dans la même tonalité, on pourra utiliser un accord ''do - mi - sol - si''♭ ({{Times New Roman|V}} de {{Times New Roman|IV}}<sup>7</sup><sub>+</sub>) avant un accord ''fa - la - do'' ({{Times New Roman|IV}}<sup>5</sup>). Le recours à une dominante secondaire peut atténuer une transition, par exemple avec un enchaînement ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> → ''fa''<sup>5</sup> (C → C<sup>7</sup> → F) qui correspond à un enchaînement {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}} : le passage ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> (C → C<sup>7</sup>) se fait en ajoutant une note (le ''si''♭) et rend naturel le passage ''do'' → ''fa''. Sur les sept degré de la gamme, on ne considère en général que cinq dominantes secondaires : en effet, la dominante du degré {{Times New Roman|I}} est la dominante « naturelle, primaire » de la tonalité (et n'est donc pas secondaire) ; et utiliser la dominante de {{Times New Roman|VII}} consisterait à considérer l'accord de {{Times New Roman|VII}} comme un accord propre, on évite donc les « {{Times New Roman|V}} de “{{Times New Roman|V}}” » (mais les « “{{Times New Roman|V}}” de {{Times New Roman|V}} » sont tout à fait « acceptables »). === Enchaînements classiques === Nous avons donc vu que l'on trouve fréquemment les enchaînements suivants : * pour créer une instabilité : ** {{Times New Roman|I}} → {{Times New Roman|V}}, ** {{Times New Roman|I}} → {{Times New Roman|IV}} (instabilité moins forte mais incertitude sur le sens d'évolution) ; * pour maintenir l'instabilité : ** {{Times New Roman|IV}} → {{Times New Roman|V}} ; * pour résoudre l'instabilité : ** {{Times New Roman|IV}} → {{Times New Roman|I}}, ** {{Times New Roman|V}} → {{Times New Roman|I}}, cas particuliers (voir plus bas) : *** {{Times New Roman|V}}<sup>+4</sup> → {{Times New Roman|I}}<sup>6</sup>, *** {{Times New Roman|I}}<sup>6</sup><sub>4</sub> → {{Times New Roman|V}}<sup>7</sup><sub>+</sub> → {{Times New Roman|I}}<sup>5</sup>. Les degrés indiqués ci-dessus sont les fonctions ; on peut donc utiliser les substitutions suivantes : * {{Times New Roman|I}} par {{Times New Roman|VI}} et, en tonalité majeure, {{Times New Roman|III}} ; * {{Times New Roman|IV}} par {{Times New Roman|II}} ; * {{Times New Roman|V}} par {{Times New Roman|VII}}. Pour enrichir l'harmonie, on peut utiliser les dominantes secondaires, en particulier : * {{Times New Roman|V}} de {{Times New Roman|V}} ({{Times New Roman|II}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|V}}, * {{Times New Roman|V}} de {{Times New Roman|IV}} ({{Times New Roman|I}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|IV}}. On peut enchaîner les enchaînements, par exemple {{Times New Roman|I}} → {{Times New Roman|IV}} → {{Times New Roman|V}}, ou encore {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}}… En jazz, on utilise très fréquemment l'enchaînement {{Times New Roman|II}} → {{Times New Roman|V}} → {{Times New Roman|I}} (deux-cinq-un). On peut bien sûr avoir d'autres enchaînements, mais ces règles permettent d'analyser un grand nombre de morceaux, et donnent des clefs utiles pour la composition. Nous voyons ci-après un certain nombre d'enchaînements courants dans différents styles == Exercice == Un hautboïste travaille la sonate en ''do'' mineur S. 277 de Heinichen. Sur le deuxième mouvement ''Allegro'', il a du mal à travailler un passage en raison des altérations accidentelles. Sur la suggestion de sa professeure, il décide d'analyser la progression d'accords sous-jacente afin que les altérations deviennent logiques. Il s'agit d'un duo hautbois et basson pour lequel les accords ne sont pas chiffrés, le basson étant ici un instrument soliste et non pas un élément de la basse continue. Sur l'extrait suivant, déterminez les basses et la qualité (chiffrage) des accords sous-jacents. Commentez. [[Fichier:Sonate hautbois basson heinichen 2e mvt mes49.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen.]] {{note|L'œuvre est en ''do'' mineur et devrait donc avoir trois bémols à la clef, or ici il n'y en a que deux. En effet, le ''la'' pouvant être bécarre en mode mineur mélodique ascendant, le compositeur a préféré le noter explicitement en altération accidentelle lorsque l'on est en mode mélodique naturel, harmonique ou mélodique descendant. C'est un procédé assez courant à l'époque baroque.}} {{boîte déroulante/début|titre=Solution}} Une des difficultés ici est que les arpèges joués par les instruments sont agrémentés de notes de passage. Les notes de la basse (du basson) sont différentes entre le premier et le deuxième temps de chaque mesure et ne peuvent pas appartenir au même accord. On a donc un accord par temps. Sur le premier temps de chaque mesure, le basson joue une octave. La note concernée est donc la basse de chaque accord. Pour savoir s'il s'agit d'un accord à l'état fondamental ou d'un renversement, on regarde ce que joue le hautbois : dans un mouvement conjoint (succession d'intervalles de secondes), il est difficile de distinguer les notes de l'arpège des notes de passage, mais : les notes des grands intervalles font partie de l'accord. Ainsi, sur le premier temps de la première mesure (la basse est un ''mi''♭), on a une sixte descendante ''sol''-''si''♭ et, à la fin du temps, une tierce descendante ''sol''-''mi''♭. L'accord est donc ''mi''♭-''sol''-''si''♭, c'est un accord de quinte (accord parfait à l'état fondamental). À la fin du premier temps, le basson joue un ''do'', c'est donc une note étrangère. Sur le second temps de la première mesure, le basson joue une tierce ascendante ''fa''-''la''♭, la première note est la basse de l'accord et la seconde une des notes de l'accord. Le hautbois commence par une sixte descendante ''la''♭-''do'', l'accord est donc ''fa''-''la''♭-''do'', un accord de quinte (accord parfait à l'état fondamental). Le ''do'' du basson la fin du premier temps est donc une anticipation. Les autres notes étrangères de la première mesure sont des notes de passage. Mais il faut faire attention : en suivant ce principe, sur les premiers temps des deuxième et troisième mesure, nous aurions des accords de septième d'espèce (puisque la septième est majeure). Or, on ne trouve pas, ou alors exceptionnellement, d'accord de septième d'espèce dans le baroque, mais quasi exclusivement des accords de septième de dominante. Donc au début de la deuxième mesure, le ''la''♮ est une appoggiature du ''si''♭, l'accord est donc ''si''♭-''ré''-''fa'', un asscord de quinte. De même, au début de la troisième mesure, le ''sol'' est une appoggiature du ''la''♭. Il faut donc se méfier d'une analyse purement « mathématique ». Il faut s'attacher à ressentir la musique, et à connaître les styles, pour faire une analyse pertinente. Ci-dessous, nous avons grisé les notes étrangères. [[Fichier:Sonate hautbois basson heinichen 2e mvt mes49 analyse.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen. Analyse de la progression harmonique.]] Le chiffrage jazz équivalent est : : | E♭ F– | B♭<sup>Δ</sup> E♭ | A♭<sup>Δ</sup> D– | G … Nous remarquons une progression assez régulière : : ''mi''♭ ↗[2<sup>de</sup>] ''fa'' | ↘[5<sup>te</sup>] ''si''♭ ↗[4<sup>te</sup>] ''mi''♭ | ↘[5<sup>te</sup>] ''la''♭ ↗[4<sup>te</sup>] ''ré'' | ↘[5<sup>te</sup>] ''sol'' Le ''mi''♭ est le degré {{Times New Roman|III}} de la tonalité principale (''do'' mineur), c'est donc une tonique faible ; il « joue le même rôle » qu'un ''do''. S'il y avait eu un accord de ''do'' au début de l'extrait, on aurait eu une progression parfaitement régulière ↗[4<sup>te</sup>] ↘[5<sup>te</sup>]. Nous avons les modulations suivantes : * mesure 49 : ''do'' mineur naturel (le ''si''♭ n'est pas une sensible) avec un accord sur “{{Times New Roman|I}}” (tonique faible, {{Times New Roman|III}}, pour la première analyse, ou bien tonique forte, {{Times New Roman|I}}, pour la seconde) suivi d'un accord sur {{Times New Roman|IV}} ; * mesure 50 : ''si''♭ majeur avec un accord sur {{Times New Roman|I}} suivi d'un accord sur {{Times New Roman|IV}} ; * mesure 51 : ''la''♭ majeur avec un accord sur {{Times New Roman|I}}, et emprunt à ''do'' majeur avec un accord sur {{Times New Roman|II}} ({{Times New Roman|IV}} faible). On a donc une marche harmonique {{Times New Roman|I}} → {{Times New Roman|IV}} qui descend d'une seconde majeure (un ton) à chaque mesure (''do'' → ''si''♭ → ''la''♭), avec une exception sur la dernière mesure (modulation en cours de mesure et descente d'une seconde mineure au lieu de majeure). Ce passage est donc construit sur une régularité, une règle qui crée un effet d'attente — enchaînement {{Times New Roman|I}}<sup>5</sup> → {{Times New Roman|IV}}<sup>5</sup> avec une marche harmonique d'une seconde majeure descendante —, et des « surprises », des exceptions au début — ce n'est pas un accord {{Times New Roman|I}}<sup>5</sup> mais un accord {{Times New Roman|III}}<sup>5</sup> — et à la fin — modulation en milieu de mesure et dernière descente d'une seconde mineure (½t ''la''♭ → ''sol''). L'extrait ne permet pas de le deviner, mais la mesure 52 est un retour en ''do'' mineur, avec donc une modulation sur la dominante (accord de ''sol''<sup>7</sup><sub>+</sub>, G<sup>7</sup>). {{boîte déroulante/fin}} == Progression d'accords == Comme pour la mélodie, la succession des accords dans un morceau, la progression d'accords, suit des règles. Et comme pour la mélodie, les règles diffèrent d'un style musical à l'autre et la créativité consiste à parfois ne pas suivre ces règles. Et comme pour la mélodie, on part d'un ensemble de notes organisé, d'une gamme caractéristique d'une tonalité, d'un mode. Les accords les plus utilisés pour une tonalité donnée sont les accords dont la fondamentale sont les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la tonalité, en particulier la triade {{Times New Roman|I}}, appelée « accord parfait » ou « accord de tonique », et l'accord de septième {{Times New Roman|V}}, appelé « septième de dominante ». Le fait d'avoir une progression d'accords qui se répète permet de structurer un morceau. Pour les morceaux courts, il participe au plaisir de l'écoute et facilite la mémorisation (par exemple le découpage couplet-refrain d'une chanson). Sur les morceaux longs, une trop grande régularité peut introduire de la lassitude, les longs morceaux sont souvent découpés en parties présentant chacune une progression régulière. Le fait d'avoir une progression régulière permet la pratique de l'improvisation : cadence en musique classique, solo en jazz et blues. ; Note : Le terme « cadence » désigne plusieurs choses différentes, et notamment en harmonie : :* une partie improvisée dans un opéra ou un concerto, sens utilisé ci-dessus ; :* une progression d'accords pour ponctuer un morceau et en particulier pour le conclure, sens utilisé dans la section suivante. === Accords peu utilisés === En mode mineur, l'accord de quinte augmentée {{Times New Roman|III<sup>+5</sup>}} est très peu utilisé. C'est un accord dissonant ; il intervient en général comme appogiature de l'accord de tonique (par exemple en ''la'' mineur : {{Times New Roman|III<sup>+5</sup>}} ''do'' - ''mi'' - ''sol''♯ → {{Times New Roman|I<sup>6</sup>}} ''do'' - ''mi'' - ''la''), ou de l'accord de dominante ({{Times New Roman|III<sup>6</sup><sub>+3</sub>}} ''mi'' - ''sol''♯ - ''do'' → {{Times New Roman|V<sup>5</sup>}} ''mi'' - ''sol''♯ - ''si''). Il peut être aussi utilisé comme préparation à l'accord de sous-dominante (enchaînement {{Times New Roman|III}} → {{Times New Roman|IV}}). Par ailleurs, il a une constitution symétrique — c'est l'empilement de deux tierces majeures — et ses renversements ont les mêmes intervalles à l'enharmonie près (quinte augmentée/sixte mineure, tierce majeure/quarte diminuée). De ce fait, un même accord est commun, par renversement et à l'enharmonie près, à trois tonalités : le premier renversement de l'accord ''do'' - ''mi'' - ''sol''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur) est enharmonique à ''mi'' - ''sol''♯ - ''si''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''do''♯ mineur) ; le second renversement est enharmonique à ''la''♭ - ''do'' - ''mi'' ({{Times New Roman|III}}<sup>e</sup> degré de ''fa'' mineur). === Accords très utilisés === Les trois accords les plus utilisés sont les accords de tonique (degré {{Times New Roman|I}}), de sous-dominante ({{Times New Roman|IV}}) et de dominante ({{Times New Roman|V}}). Ils interviennent en particulier en fin de phrase, dans les cadences. L'accord de dominante sert souvent à introduire une modulation : la modulation commence sur l'accord de dominante de la nouvelle tonalité. On note que l'accord de sous-dominante est situé une quinte juste en dessous de la tonique, les accords de dominante et de sous-dominante sont donc symétriques. En jazz, on utilise également très fréquemment l'accord de la sus-tonique (degré {{Times New Roman|II}}), souvent dans des progressions {{Times New Roman|II}} - {{Times New Roman|V}} (- {{Times New Roman|I}}). Rappelons que l'accord de sus-tonique a la fonction de sous-dominante. === Cadences et ''turnaround'' === Le terme « cadence » provient de l'italien ''cadenza'' et désigne la « chute », la fin d'un morceau ou d'une phrase musicale. On distingue deux types de cadences : * les cadences conclusive, qui créent une sensation de complétude ; * les cadences suspensives, qui crèent une sensation d'attente. ==== Cadence parfaite ==== [[Fichier:Au clair de le lune cadence parfaite.midi|thumb|''Au clair de la lune'', harmonisé avec une cadence parfaite (italienne).]] [[Fichier:Au clair de le lune mineur cadence parfaite.midi|thumb|''Idem'' mais en mode mineur harmonique.]] La cadence parfaite est l'enchaînement de l'accord de dominante suivi de l'accord parfait : {{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}}, les deux accord étant à l'état fondamental. Elle donne une impression de stabilité et est donc très souvent utilisée pour conclure un morceau. C'est une cadence conclusive. On peut aussi utiliser l'accord de septième de dominante, la dissonance introduisant une tension résolue par l'accord parfait : {{Times New Roman|V<sup>7</sup><sub>+</sub> - I<sup>5</sup>}}. Elle est souvent précédée de l'accord construit sur le IV<sup>e</sup> degré, appelé « accord de préparation », pour former la cadence italienne : {{Times New Roman|IV<sup>5</sup> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}}. Elle est également souvent précédée du second renversement de l'accord de tonique, qui est alors appelé « appoggiature de la cadence » : {{Times New Roman|I<sup>6</sup><sub>4</sub> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}} (on remarque que les accords {{Times New Roman|I}}<sup>6</sup><sub>4</sub> et {{Times New Roman|V}}<sup>5</sup> ont la basse en commun, et que l'on peut passer de l'un à l'autre par un mouvement conjoint sur les autres notes). {{clear}} ==== Demi-cadence ==== [[Fichier:Au clair de le lune demi cadence.midi|thumb|''Au clair de la lune'', harmonisé avec une demi-cadence.]] Une demi-cadence est une phrase ou un morceau se concluant sur l'accord construit sur le cinquième degré. Il provoque une sensation d'attente, de suspens. Il s'agit en général d'une succession {{Times New Roman|II - V}} ou {{Times New Roman|IV - V}}. C'est une cadence suspensive. On utilise rarement un accord de septième de dominante. {{clear}} ==== Cadence rompue ou évitée ==== La cadence rompue, ou cadence évitée, est succession d'un accord de dominante et d'un accord de sus-dominante, {{Times New Roman|V}} - {{Times New Roman|VI}}. C'est une cadence suspensive. ==== Cadence imparfaite ==== Une cadence imparfaite est une cadence {{Times New Roman|V - I}}, comme la cadence parfaite, mais dont au moins un des deux accords est dans un état renversé. ==== Cadence plagale ==== La cadence plagale — du grec ''plagios'', oblique, en biais — est la succession de l'accord construit sur le quatrième degré, suivi de l'accord parfait : {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}. Elle peut être utilisée après une cadence parfaite ({{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}} - {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}). Elle donne un caractère solennel, voire religieux — elle est parfois appelée « cadence amen » —, elle a un côté antique qui rappelle la musique modale et médiévale<ref>{{lien web |url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/la-cadence-amen-ou-comment-se-dire-adieu-7191921 |titre=La cadence « Amen » ou comment se dire adieu |auteur=Max Dozolme (MAXXI Classique) |site=France Musique |date=2025-04-25 |consulté le=2025-04-25}}.</ref>. C'est une cadence conclusive. ==== {{lang|en|Turnaround}} ==== [[Fichier:Au clair de le lune turnaround.midi|thumb|Au clair de la lune, harmonisé en style jazz : accords de 7{{e}}, anatole suivie d'un ''{{lang|en|turnaround}}'' ii-V-I.]] Le terme ''{{lang|en|turnaround}}'' signifie revirement, retournement. C'est une succession d'accords que fait la transition entre deux parties, en créant une tension-résolution. Le ''{{lang|en|turnaround}}'' le plus courant est la succession {{Times New Roman|II - V - I}}. On utilise également fréquemment l'anatole : {{Times New Roman|I - VI - II - V}}. {| class="wikitable" |+ Progressions typiques d'accords dans une tonalité majeure ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Cadence<br />parfaite<br /> {{Times New Roman|V - I}} ! scope="col" | Cadence<br />italienne<br />{{Times New Roman|IV - V - I}} ! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou IV - V}} ! scope="col" | Cadence<br />plagale<br />{{Times New Roman|IV - I}} ! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}} ! scope="col" | Anatole<br />{{Times New Roman|I - vi - ii - V}} |- |''Do'' majeur || || G - C || F - G - C || Dm - G ou F - G || F - C || Dm - G - C || C - Am - Dm - G |- |''Sol'' majeur || ''fa''♯ || D - G || C - D - G || Am - D ou C - D || C - G || Am - D - G || G - Em - Am - D |- |''Ré'' majeur || ''fa''♯, ''do''♯ || A - D || G - A - D || Em - A ou G - A || G - D || Em - A - D || D - Bm - Em - A |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || E - A || D - E - A || Bm - E ou D - E || D - A || Bm - E - A || A - F♯m - B - E |- | ''Fa'' majeur || ''si''♭ || C - F || B♭ - C - F || Gm - C ou B♭ - C || B♭ - F || Gm - C - F || F - Dm - Gm - C |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || F - B♭ || E♭ - F - B♭ || Cm - F ou E♭ - F || E♭ - B♭ || Cm - F - B♭ || B♭ - Gm - Cm - F |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || B♭ - E♭ || A♭ - B♭ - E♭ || Fm - B♭ ou A♭ - B♭ || A♭ - E♭ || Fm - B♭ - E♭ || Gm - Cm - Fm - B♭ |} {| class="wikitable" |+ Progressions typiques d'accords dans une tonalité mineure ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Cadence<br />parfaite<br />{{Times New Roman|V - i}} ! scope="col" | Cadence<br />italienne<br />{{Times New Roman|iv - V - i}} ! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou iv - V}} ! scope="col" | Cadence<br />plagale<br />{{Times New Roman|iv - i}} ! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}} ! scope="col" | Anatole<br />{{Times New Roman|i - VI - ii - V}} |- | ''La'' mineur<br />harmonique || || E - Am || Dm - E - Am || B° - E ou Dm - E || Dm - Am || B° - E - Am || Am - F - B° - E |- | ''Mi'' mineur<br />harmonique || ''fa''♯ || B - Em || Am - B - Em || F♯° - B ou Am - B || Am - Em || F♯° - B - Em || Em - C - F♯° - B |- | ''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || F♯ - Bm || Em - F♯ - Bm || C♯° - F♯ ou Em - F♯ || Em - Bm || C♯° - F♯ - Bm || Bm - G - C♯° - F♯ |- | ''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || C♯ - F♯m || Bm - C♯ - F♯m || G♯° - C♯ ou Bm - C♯ || Bm - F♯m || G♯° - C♯ - F♯m || A+ - D - G♯° - C♯ |- | ''Ré'' mineur<br />harmonique || ''si''♭ || A - Dm || Gm - A - Dm || E° - A ou Gm - A || Gm - Dm || E° - A - Dm || Dm - B♭ - E° - A |- | ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || D - Gm || Cm - D - Gm || A° - D ou Cm - D || Cm - Gm|| A° - D - Gm || Gm - E♭ - A° - D |- | ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || G - Cm || Fm - G - Cm || D° - G ou Fm - G || Fm - Dm || D° - G - Cm || Cm - A♭ - D° - G |} ==== Exemple : ''La Mer'' ==== : {{lien web | url = https://www.youtube.com/watch?v=PXQh9jTwwoA | titre = Charles Trenet - La mer (Officiel) [Live Version] | site = YouTube | auteur = Charles Trenet | consulté le = 2020-12-24 }} Le début de ''La Mer'' (Charles Trenet, 1946) est en ''do'' majeur et est harmonisé par l'anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (C - Am - Dm - G<sup>7</sup>) sur deux mesures, jouée deux fois ({{Times New Roman|1=<nowiki>|I-vi|ii-V</nowiki><sup>7</sup><nowiki>|</nowiki>}} × 2). Viennent des variations avec les progressions {{Times New Roman|I-III-vi-V<sup>7</sup>}} (C - E - Am - G<sup>7</sup>) puis la « progression ’50s » (voir plus bas) {{Times New Roman|I-vi-IV-VI<sup>7</sup>}} (C - Am - F - A<sup>7</sup>, on remarque que {{Times New Roman|IV}}/F est le relatif majeur du {{Times New Roman|ii}}/Dm de l'anatole), jouées chacune une fois sur deux mesure ; puis cette première partie se conclut par une demie cadence {{Times New Roman|ii-V<sup>7</sup>}} sur une mesure puis une dernière anatole sur trois mesures ({{Times New Roman|1=<nowiki>|I-vi|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}}). Cela constitue une première partie « A » sur douze mesures qui se termine par une demi-cadence ({{Times New Roman|ii-V<sup>7</sup>}}) qui appelle une suite. Cette partie A est jouée une deuxième fois mais la fin est modifiée pour la transition : les deux dernières mesures {{Times New Roman|<nowiki>|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}} deviennent {{Times New Roman|<nowiki>|ii-V</nowiki><sup>7</sup><nowiki>|I|</nowiki>}} (|Dm-G7|C|), cette partie « A’ » se conclut donc par une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}). Le morceau passe ensuite en tonalité de ''mi'' majeur, donc une tierce au dessus de ''do'' majeur, sur six mesures. Cette partie utilise une progression ’50s {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (E - C♯m - A - B<sup>7</sup>), qui est rappelons-le une variation de l'anatole, l'accord {{Times New Roman|ii}} (Fm) étant remplacé par son relatif majeur {{Times New Roman|IV}} (A). Cette anatole modifiée est jouée deux fois puis la partie en ''mi'' majeur se conclut par l'accord parfait {{Times New Roman|I}} joué sur deux mesures (|E|E|), on a donc, avec la mesure précédente, avec une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}). Suivent ensuite six mesures en ''sol'' majeur, donc à nouveau une tierce au dessus de ''mi'' majeur. Elle comporte une progression {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (G - Em - C - D<sup>7</sup>), donc anatole avec substitution du {{Times New Roman|ii}}/Am par son relatif majeur {{Times New Roman|VI}}/C (progression ’50s), puis une anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (G - Em - Am - D<sup>7</sup>) et deux mesure sur la tonique {{Times New Roman|I-I<sup>7</sup>}} (G - G<sup>7</sup>), formant à nouveau une cadence parfaite. La fin sur un accord de septième, dissonant, appelle une suite. Cette partie « B » de douze mesures comporte donc deux parties similaires « B1 » et « B2 » qui forment une marche harmonique (montée d'une tierce). Le morceau se conclut par une reprise de la partie « A’ » et se termine donc par une cadence parfaite. Nous avons une structure A-A’-B-A’ sur 48 mesures, proche la forme AABA étudiée plus loin. Donc ''La Mer'' est un morceau structuré autour de l'anatole avec des variations (progression ’50s, substitution du {{Times New Roman|ii}} par son relatif majeur {{Times New Roman|IV}}) et comportant une marche harmonique dans sa troisième partie. Les parties se concluent par des ''{{lang|en|turnarounds}}'' sous la forme d'une cadence parfaite ou, pour la partie A, par une demi-cadence. {| border="1" rules="rows" frame="hsides" |+ Structure de ''La Mer'' |- align="center" | | colspan="12" | ''do'' majeur | |- align="center" ! scope="row" rowspan=2 | A | colspan="2" | anatole | colspan="2" | // | colspan="2" | variation | colspan="2" | ’50s | ½ c. | colspan="3" | anatole | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii}} || <nowiki>|</nowiki> {{Times New Roman|V<sup>7</sup>}} || <nowiki>|</nowiki> |- align="center" ! scope="row" rowspan="2" | A’ | colspan="2" | anatole | colspan="2" | // | colspan="2" | variation | colspan="2" | ’50s | ½ c. | colspan="2" | anatole | c.p. | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> |- align="center" | | colspan="6" | B1 : ''mi'' majeur | colspan="6" background="lightgray" | B2 : ''sol'' majeur | |- align="center" ! scope="row" rowspan="2" | B | colspan="2" | ’50s | colspan="2" | // |colspan="2" | c.p. | colspan="2" | ’50s | colspan="2" | // |colspan="2" | c.p. | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I<sup>7</sup>}} || <nowiki>|</nowiki> |- ! scope="row" | A’ | colspan="12" | | |} === Progression blues === La musique blues est apparue dans les années 1860. Elle est en général bâtie sur une grille d'accords ''({{lang|en|changes}})'' immuable de douze mesures ''({{lang|en|twelve-bar blues}})''. C'est sur cet accompagnement qui se répète que s'ajoute la mélodie — chant et solo. Cette structure est typique du blues et se retrouve dans ses dérivés comme le rock 'n' roll. Le rythme est toujours un rythme ternaire syncopé ''({{lang|en|shuffle, swing, groove}}, ''notes inégales'')'' : la mesure est à quatre temps, mais la noire est divisée en noire-croche en triolet, ou encore triolet de croche en appuyant la première et la troisième. La mélodie se construit en général sur une gamme blues de six degrés (gamme pentatonique mineure avec une quarte augmentée), mais bien que la gamme soit mineure, l'harmonie est construite sur la gamme majeure homonyme : un blues en ''fa'' a une mélodie sur la gamme de ''fa'' mineur, mais une harmonie sur la gamme de ''fa'' majeur. La grille d'accord comporte les accords construits sur les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la gamme majeure homonyme. Les accords sont souvent des accords de septième (donc avec une tierce majeure et une septième mineure), il ne s'agit donc pas d'une harmonisation de gamme diatonique (puisque la septième est majeure sur l'accord de tonique). Par exemple, pour un blues en ''do'' : * accord parfait de do majeur, C ({{Times New Roman|I}}<sup>er</sup> degré) ; * accord parfait de fa majeur, F ({{Times New Roman|IV}}<sup>e</sup> degré) ; * accord parfait de sol majeur, G ({{Times New Roman|V}}<sup>e</sup> degré). Il existe quelques morceaux harmonisés avec des accords mineurs, comme par exemple ''As the Years Go Passing By'' d'Albert King (Duje Records, 1959). La progression blues est organisée en trois blocs de quatre mesures ayant les fonctions suivantes (voir ci-dessus ''[[#Harmonie fonctionnelle|Harmonie fonctionnelle]]'') : * quatre mesures toniques ; * quatre mesures sous-dominantes ; * quatre mesures dominantes. La forme la plus simple, que Jeff Gardner appelle « forme A », est la suivante : {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, forme A |- ! scope="row" | Tonique | width="50px" | I | width="50px" | I | width="50px" | I | width="50px" | I |- ! scope="row" | Sous-domminante | width="50px" | IV | width="50px" | IV | width="50px" | I | width="50px" | I |- ! scope="row" | Dominante | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | V |} La progression {{Times New Roman|I-V}} des deux dernières mesures forment le ''{{lang|en|turnaround}}'', la demie cadence qui lance le cycle suivant. Nous présentons ci-dessous un exemple typique de ligne de basse ''({{lang|en|walking bass}})'' pour le ''{{lang|en|turnaround}}'' d'un blues en ''la'' : [[Fichier:Turnaround classique blues en la.svg|Exemple typique de ligne de basse pour un ''turnaround'' de blues en ''la''.]] [[Fichier:Blues mi harmonie elementaire.midi|thumb|Blues en ''mi'', harmonisé de manière élémentaire avec une ''{{lang|en|walking bass}}''.]] Vous pouvez écouter ci-contre une harmonisation typique d'un blues en ''mi''. Les accords sont exécutés par une basse marchante ''({{lang|en|walking bass}})'', qui joue une arpège sur la triade avec l'ajout d'une sixte majeure et d'une septième mineure, et par une guitare qui joue un accord de puissance ''({{lang|en|power chord}})'', qui n'est composé que de la fondamentale et de la quinte juste, avec une sixte en appoggiature. La forme B s'obtient en changeant la deuxième mesure : on joue un degré {{Times New Roman|IV}} au lieu d'un degré {{Times New Roman|I}}. La progression {{Times New Roman|I-IV}} sur les deux premières mesures est appelé ''{{lang|en|quick change}}''. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, forme B |- | width="50px" | I | width="50px" | IV | width="50px" | I | width="50px" | I |- | width="50px" | IV | width="50px" | IV | width="50px" | I | width="50px" | I |- | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | V |} Par exemple, ''Sweet Home Chicago'' (Robert Johnson, 1936) est un blues en ''fa'' ; sa grille d'accords, aux variations près, suit une forme B : {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression de ''Sweet Home Chicago'' |- | width="50px" | F | width="50px" | B♭ | width="50px" | F | width="50px" | F |- | width="50px" | B♭ | width="50px" | B♭ | width="50px" | F | width="50px" | F |- | width="50px" | C7 | width="50px" | B♭7 | width="50px" | F7 | width="50px" | C7 |} : Écouter {{lien web | url =https://www.youtube.com/watch?v=dkftesK2dck | titre = Robert Johnson "Sweet Home Chicago" | auteur = Michal Angel | site = YouTube | date = 2007-12-09 | consulté le = 2020-12-17 }} Les formes C et D s'obtiennent à partir des formes A et B en changeant le dernier accord par un accord sur le degré {{Times New Roman|I}}, ce qui forme une cadence plagale. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, formes C et D |- | colspan="4" | … |- | colspan="4" | … |- | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | I |} L'harmonie peut être enrichie, notamment en jazz. Voici par exemple une grille du blues souvent utilisés en bebop. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Exemple de progression de blues bebop sur une base de forme B |- | width="60px" | I<sup>7</sup> | width="60px" | IV<sup>7</sup> | width="60px" | I<sup>7</sup> | width="60px" | V–<sup>7</sup> <nowiki>|</nowiki> I<sup>7</sup> |- | width="60px" | IV<sup>7</sup> | width="60px" | IV<sup>7</sup> | width="60px" | I<sup>7</sup> | width="60px" | VI<sup>7 ♯9 ♭13</sup> |- | width="60px" | II–<sup>7</sup> | width="60px" | V<sup>7</sup> | width="60px" | V<sup>7</sup> <nowiki>|</nowiki> IV<sup>7</sup> | width="60px" | II–<sup>7</sup> <nowiki>|</nowiki> V<sup>7</sup> |} On peut aussi trouver des blues sur huit mesures, sur seize mesures comme ''Watermelon Man'' de Herbie Hancock (album ''Takin' Off'', Blue Note, 1962) ou ''Let's Dance'' de Jim Lee (interprété par Chris Montez, Monogram, 1962) * {{lien web |url= https://www.dailymotion.com/video/x5iduwo |titre=Herbie Hancock - Watermelon Man (1962) |auteur=theUnforgettablesTv |site=Dailymotion |date=2003 |consulté le=2021-02-09 }} * {{lien web |url=https://www.youtube.com/watch?v=6JXshurYONc |titre=Let's Dance |auteur=Chris Montez |site=YouTube |date=2016-08-06 |consulté le=2021-02-09 }} À l'inverse, certains blues peuvent avoir une structure plus simple que les douze mesure ; par exemple ''Hoochie Coochie Man'' de Willie Dixon (interprété par Muddy Waters sous le titre ''Mannish Boy'', Chicago Blues, 1954) est construit sur un seul accord répété tout le long de la chanson. * {{lien web |url=https://www.dailymotion.com/video/x5iduwo |titre=Muddy Waters - Hoochie Coochie Man |auteur=Muddy Waters |site=Dailymotion |date=2012 | consulté le=2021-02-09 }} === Cadence andalouse === La cadence andalouse est une progression de quatre accords, descendant par mouvement conjoint : * en mode de ''mi'' (mode phrygien) : {{Times New Roman|IV}} - {{Times New Roman|III}} - {{Times New Roman|II}} - {{Times New Roman|I}} ;<br />par exemple en ''mi'' phrygien : Am - G - F - E ; en ''do'' phrygien : Fm - E♭ - D♭ - C ;<br />on notera que le degré {{Times New Roman|III}} est diésé dans l'accord final (ou bécarre s'il est bémol dans la tonalité) ; * en mode mineur : {{Times New Roman|I}} - {{Times New Roman|VII}} - {{Times New Roman|VI}} - {{Times New Roman|V}} ;<br />par exemple en ''la'' mineur : Am - G - F - E ; en ''do'' mineur : Cm - B♭ - A♭ - m ;<br />comme précédemment, on notera que le degré {{Times New Roman|VII}} est diésé dans l'accord final. === Progressions selon le cercle des quintes === [[Fichier:Cercle quintes degres tonalite majeure.svg|vignette|Cercle des quinte justes (parcouru dans le sens des aiguilles d'une montre) des degrés d'une tonalité majeure.]] La progression {{Times New Roman|V-I}} est la cadence parfaite, mais on peut aussi l'employer au milieu d'un morceau. Cette progression étant courte, sa répétition crée de la lassitude ; on peut la compléter par d'autres accords séparés d'une quinte juste, en suivant le « cercle des quintes » : {{Times New Roman|I-V-IX}}, la neuvième étant enharmonique de la seconde, on obtient {{Times New Roman|I-V-II}} ; en retournant la progression, on obtient le turnaround {{Times New Roman|II-V-I}}. En jazz, dans le morceau ''Topsy'' (Edgar Battle et Eddir Durham), la partie <span style="border-color: black; border-style: solid; border-width: thin;">C</span> est composée d'une progression en quintes descendantes d'accords de septième de dominante : D<sup>7</sup> - G<sup>7</sup> - C<sup>7</sup> - F<sup>7</sup> - B♭<sup>7</sup> ; les improvisatiosn sur cette partie peuvent donc se faire en modulant en ''sol'' (majeur ou mineur) - ''do'' (M/m) - ''fa'' (M/m) - ''si''♭ (M/m) - ''mi''♭ (M/m). On peut continuer de décrire le cercle des quintes : {{Times New Roman|I-V-II-VI}}, on obtient l'anatole dans le désordre ; on peut à l'inverse étendre les quintes vers la gauche, {{Times New Roman|IV-I-V-II-VI}}. En musique populaire, on trouve fréquemment une progression fondée sur les accord {{Times New Roman|I}}, {{Times New Roman|IV}}, {{Times New Roman|V}} et {{Times New Roman|VI}}, popularisée dans les années 1950. La « progression années 1950 », « progression ''{{lang|en|fifties ('50)}}'' » ''({{lang|en|'50s progression}})'' est dans l'ordre {{Times New Roman|I-VI-IV-V}}. On trouve aussi cette progression en musique classique. Si la tonalité est majeure, la triade sur la sus-dominante est mineure, les autres sont majeures, on notera donc souvent {{Times New Roman|I-vi-IV-V}}. On peut avoir des permutations circulaires (le dernier accord venant au début, ou vice-versa) : {{Times New Roman|vi-IV-V-I}}, {{Times New Roman|IV-V-I-vi}} et {{Times New Roman|V-I-vi-IV}}. {| class="wikitable" |+ Accords selon la tonalité ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" style="font-family:Times New Roman" | I ! scope="col" style="font-family:Times New Roman" | IV ! scope="col" style="font-family:Times New Roman" | V ! scope="col" style="font-family:Times New Roman" | vi |- |''Do'' majeur || || C || F || G || Am |- |''Sol'' majeur || ''fa''♯ || G || C || D || Em |- |''Ré'' majeur || ''fa''♯, ''do''♯ || D || G || A || Bm |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A || D || E || F♯m |- | ''Fa'' majeur || ''si''♭ || F || B♭ || C || Dm |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭ || E♭ || F || Gm |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭ || A♭ || B♭ || Cm |} Par exemple, en tonalité de ''do'' majeur, la progression {{Times New Roman|I-vi-IV-V}} sera C-Am-F-G. Il existe d'autres progressions utilisant ces accords mais dans un autre ordre, typiquement {{Times New Roman|I–IV–vi–V}} ou une de ses permutations circulaires : {{Times New Roman|IV–vi–V-I}}, {{Times New Roman|vi–V-I-IV}} ou {{Times New Roman|V-I-IV-vi}}. Ou dans un autre ordre. PV Nova l'illustre dans plusieurs de ses « expériences » dans la version {{Times New Roman|vi-V-IV-I}}, soit Am-G-F-C, ou encore {{Times New Roman|vi-IV-I-V}}, soit Am-F-C-G : : {{lien web | url = https://www.youtube.com/watch?v=w08LeZGbXq4 | titre = Expérience n<sup>o</sup> 6 — La Happy Pop | auteur = PV Nova | site = YouTube | date = 2011-08-20 | consulté le = 2020-12-13 }} et cela devient un gag récurrent avec son « chapeau des accords magiques qu'on nous ressort à toutes les sauces » : {{lien web | url = https://www.youtube.com/watch?v=VMY_vc4nZAU | titre = Expérience n<sup>o</sup> 14 — La Soupe dou Brasil | auteur = PV Nova | site = YouTube | date = 2012-10-03 | consulté le = 2020-12-17 }} Cette récurrence est également parodiée par le groupe The Axis of Awesome avec ses « chansons à quatre accords » ''({{lang|en|four-chords song}})'', dans une sketch où ils mêlent 47 chansons en utilisant l'ordre {{Times New Roman|I-V-vi-IV}} : : {{lien web | url = https://www.youtube.com/watch?v=oOlDewpCfZQ | titre = 4 Chords | Music Videos | The Axis Of Awesome | auteur = The Axis of Awesome | site = YouTube | date = 2011-07-20 | consulté le = 2020-12-17 }} {{boîte déroulante/début|titre=Chansons mêlées dans le sketch}} # Journey : ''Don't Stop Believing'' ; # James Blunt : ''You're Beautiful'' ; # Black Eyed Peas : ''Where Is the Love'' ; # Alphaville : ''Forever Young'' ; # Jason Mraz : ''I'm Yours'' ; # Train : ''Hey Soul Sister'' ; # The Calling : ''Wherever You Will Go'' ; # Elton John : ''Can You Feel The Love Tonight'' (''Le Roi lion'') ; # Akon : ''Don't Matter'' ; # John Denver : ''Take Me Home, Country Roads'' ; # Lady Gaga : ''Paparazzi'' ; # U2 : ''With Or Without You'' ; # The Last Goodnight : ''Pictures of You'' ; # Maroon Five : ''She Will Be Loved'' ; # The Beatles : ''Let It Be'' ; # Bob Marley : ''No Woman No Cry'' ; # Marcy Playground : ''Sex and Candy'' ; # Men At Work : ''Land Down Under'' ; # thème de ''America's Funniest Home Videos'' (équivalent des émissions ''Vidéo Gag'' et ''Drôle de vidéo'') ; # Jack Johnson : ''Taylor'' ; # Spice Girls : ''Two Become One'' ; # A Ha : ''Take On Me'' ; # Green Day : ''When I Come Around'' ; # Eagle Eye Cherry : ''Save Tonight'' ; # Toto : ''Africa'' ; # Beyonce : ''If I Were A Boy'' ; # Kelly Clarkson : ''Behind These Hazel Eyes'' ; # Jason DeRulo : ''In My Head'' ; # The Smashing Pumpkins : ''Bullet With Butterfly Wings'' ; # Joan Osborne : ''One Of Us'' ; # Avril Lavigne : ''Complicated'' ; # The Offspring : ''Self Esteem'' ; # The Offspring : ''You're Gonna Go Far Kid'' ; # Akon : ''Beautiful'' ; # Timberland featuring OneRepublic : ''Apologize'' ; # Eminem featuring Rihanna : ''Love the Way You Lie'' ; # Bon Jovi : ''It's My Life'' ; # Lady Gaga : ''Pokerface'' ; # Aqua : ''Barbie Girl'' ; # Red Hot Chili Peppers : ''Otherside'' ; # The Gregory Brothers : ''Double Rainbow'' ; # MGMT : ''Kids'' ; # Andrea Bocelli : ''Time To Say Goodbye'' ; # Robert Burns : ''Auld Lang Syne'' ; # Five for fighting : ''Superman'' ; # The Axis of Awesome : ''Birdplane'' ; # Missy Higgins : ''Scar''. {{boîte déroulante/fin}} Vous pouvez par exemple jouer les accords C-G-Am-F ({{Times New Roman|I-V-vi-IV}}) et chanter dessus ''{{lang|en|Let It Be}}'' (Paul McCartney, The Beattles, 1970) ou ''Libérée, délivrée'' (Robert Lopez, ''La Reine des neiges'', 2013). La progression {{Times New Roman|I-V-vi-IV}} est considérée comme « optimiste » tandis que sa variante {{Times New Roman|iv-IV-I-V}} est considérée comme « pessimiste ». On peut voir la progression {{Times New Roman|I-vi-IV-V}} comme une variante de l'anatole {{Times New Roman|I-vi-ii-V}}, obtenue en remplaçant l'accord de sustonique {{Times New Roman|ii}} par l'accord de sous-dominante {{Times New Roman|IV}} (son relatif majeur, et degré ayant la même fonction). ==== Exemples de progression selon le cercle des quintes en musique classique ==== [[Fichier:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.midi|vignette|Dietrich Buxtehude, Psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]] Cette progression selon la cercle des quintes, sous la forme {{Times New Roman|I-vi-IV-V}}, apparaît déjà au {{pc|xvii}}<sup>e</sup> siècle dans le psaume 42 ''Quem ad modum desiderat cervis'' (BuxVW92) de Dietrich Buxtehude (1637-1707). Le morceau est en ''fa'' majeur, la progression d'accords est donc F-Dm-B♭-C. : {{lien web | url = https://www.youtube.com/watch?v=8FmV9l1RqSg | titre = D. Buxtehude - Quemadmodum desiderat cervus, BuxWV 92 | auteur = Longobardo | site = YouTube | date = 2013-04-06 | consulté la = 2021-01-01 }} [[File:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.svg|vignette|450x450px|center|Dietrich Buxtehude, psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]] {{clear}} [[Fichier:JSBach BWV140 cantate 4 mesures.midi|vignette|J.-S. Bach, cantate BWV140, quatre premières mesures.]] On la trouve également dans l'ouverture de la cantate ''{{lang|de|Wachet auf, ruft uns die Stimme}}'' de Jean-Sébastien Bach (BWV140, 1731). Le morceau est en ''mi''♭ majeur, la progression d'accords est donc E♭-Cm-A♭<sup>6</sup>-B♭. [[Fichier:JSBach BWV140 cantate 4 mesures.svg|vignette|center|J.-S. Bach, cantate BWV140, quatre premières mesures.|alt=|517x517px]] {{clear}} [[Fichier:Mozart K310 Sonate8 mesures 45 a 49.midi|vignette|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]] La même progression est utilisée par Mozart, par exemple dans le premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778), la progression d'accords est C-Am-F-G qui correspond à la progression {{Times New Roman|III-i-VI-VII}} de ''la'' mineur, mais à la progression {{Times New Roman|I-vi-IV-V}} de la gamme relative, ''do'' majeur . [[Fichier:Mozart K310 Sonate8 mesures 45 a 49.svg|vignette|center|500px|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]] === Substitution tritonique === Un des accords les plus utilisés est donc l'accord de septième de dominante, {{Times New Roman|V<sup>7</sup><sub>+</sub>}} qui contient les degrés {{Times New Roman|V}}, {{Times New Roman|VII}}, {{Times New Roman|II}} ({{Times New Roman|IX}}) et {{Times New Roman|IV}}({{Times New Roman|XI}}) ; par exemple, en tonalité de ''do'' majeur, l'accord de ''sol'' septième (G<sup>7</sup>) contient les notes ''sol''-''si''-''ré''-''fa''. Si l'on prend l'accord dont la fondamentale est trois tons (triton) au-dessus ou en dessous — l'octave contenant six tons, on arrive sur la même note —, {{Times New Roman|♭II<sup>7</sup>}}, ici ''ré''♭ septième (D♭<sup>7</sup>), celui-ci contient les notes ''ré''♭-''fa''-''la''♭-''do''♭, cette dernière note étant l'enharmonique de ''si''. Les deux accords G<sup>7</sup> et D♭<sup>7</sup> ont donc deux notes en commun : le ''fa'' et le ''si''/''do''♭. Il est donc fréquent en jazz de substituer l'accord {{Times New Roman|V<sup>7</sup><sub>+</sub>}} par l'accord {{Times New Roman|♭II<sup>7</sup>}}. Par exemple, la progression {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|V<sup>7</sup>}}-{{Times New Roman|I<sup>&Delta;</sup>}} devient {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|♭II<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}}. C'est un procédé courant de réharmonisation (le fait de remplacer un accord par un autre dans un morceau existant). Les six substitutions possibles sont donc : C<sup>7</sup>↔F♯<sup>7</sup> - D♭<sup>7</sup>↔G<sup>7</sup> - D<sup>7</sup>↔A♭<sup>7</sup> - E♭<sup>7</sup>↔A<sup>7</sup> - E<sup>7</sup>↔B♭<sup>7</sup> - F<sup>7</sup>↔B<sup>7</sup>. [[Fichier:Übermäsiger Terzquartakkord.jpg|vignette|Exemple de cadence parfaite en ''do'' majeur avec substitution tritonique (sixte française).]] Dans l'accord D♭<sup>7</sup>, si l'on remplace le ''do''♭ par son ''si'' enharmonique, on obtient un accord de sixte augmentée : ''ré''♭-''fa''-''la''♭-''si''. Cet accord est utilisé en musique classique depuis la Renaissance ; on distingue en fait trois accords de sixte augmentée : * sixte française ''ré''♭-''fa''-''sol''-''si'' ; * sixte allemande : ''ré''♭-''fa''-''la''♭-''si'' ; * sixte italienne : ''ré''♭-''fa''-''si''. Par exemple, le ''Quintuor en ''ut'' majeur'' de Franz Schubert (1828) se termine par une cadence parfaite dont l'accord de dominante est remplacé par une sixte française ''ré''♭-''fa''-''si''-''sol''-''si'' (''ré''♭ aux violoncelles, ''fa'' à l'alto, ''si''-''sol'' aux seconds violons et ''si'' au premier violon). [[Fichier:Schubert C major Quintet ending.wav|vignette|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]] [[Fichier:Schubert C major Quintet ending.png|vignette|center|upright=2.5|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]] === Autres accords de substitution === Substituer un accord consiste à utiliser un accord provenant d'une tonalité étrangère à la tonalité en cours. À la différence d'une modulation, la substitution est très courte et ne donne pas l'impression de changer de tonalité ; on a juste un sentiment « étrange » passager. Un court passage dans une autre tonalité est également appelée « emprunt ». Nous avons déjà vu plusieurs méthodes de substitution : * utilisation d'une note étrangère : une note étrangère — note de passage, appoggiature, anticipation, retard… — crée momentanément un accord hors tonalité ; en musique classique, ceci n'est pas considéré comme un accord en propre, mais en jazz, on parle « d'accord de passage » et « d'accord suspendu » ; * utilisation d'une dominante secondaire : l'accord de dominante secondaire est hors tonalité ; le but ici est de faire une cadence parfaite, mais sur un autre degré que la tonique de la tonalité en cours ; * la substitution tritonique, vue ci-dessus, pour remplacer un accord de septième de dominante. Une dernière méthode consiste à remplacer un accord par un accord d'une gamme de même tonique, mais d'un autre mode ; on « emprunte » ''({{lang|en|borrow}})'' l'accord d'un autre mode. Par exemple, substituer un accord de la tonalité de ''do'' majeur par un accord de la tonalité de ''do'' mineur ou de ''do'' mode de ''mi'' (phrygien). Donc en ''do'' majeur, on peut remplacer un accord de ''ré'' mineur septième (D<sub>m</sub><sup>7</sup>) par un accord de ''ré'' demi-diminué (D<sup>⌀</sup>, D<sub>m</sub><sup>7&flat;5</sup>) qui est un accord appartenant à la donalité de ''la'' mineur harmonique. === Forme AABA === La forme AABA est composée de deux progressions de huit mesures, notées A et B ; cela représente trente-deux mesures au total, on parle donc souvent en anglais de la ''{{lang|en|32-bars form}}''. C'est une forme que l'on retrouve dans de nombreuses chanson de comédies musicales de Broadway comme ''Have You Met Miss Jones'' (''{{lang|en|I'd Rather Be Right}}'', 1937), ''{{lang|en|Over the Rainbow}}'' (''Le Magicien d'Oz'', Harold Harlen, 1939), ''{{lang|en|All the Things You Are}}'' (''{{lang|en|Very Warm for may}}'', 1939). Par exemple, la version de ''{{lang|en|Over the Rainbow}}'' chantée par Judy Garland est en ''la''♭ majeur et la progression d'accords est globalement : * A (couplet) : A♭-Fm | Cm-A♭ | D♭ | Cm-A♭ | D♭ | D♭-F | B♭-E♭ | A♭ * B (pont) : A♭ | B♭m | Cm | D♭ | A♭ | B♭-G | Cm-G | B♭m-E♭ soit en degrés : * A : {{Times New Roman|<nowiki>I-vi | iii-I | IV | iii-IV | IV | IV-vi | II-V | I</nowiki>}} * B : {{Times New Roman|<nowiki>I | ii | iii | IV | I | II-VII | iii-VII | ii-V</nowiki>}} Par rapport aux paroles de la chanson, on a * A : couplet 1 ''« {{lang|en|Somewhere […] lullaby}} »'' ; * A : couplet 2 ''« {{lang|en|Somewhere […] really do come true}} »'' ; * B : pont ''« {{lang|en|Someday […] you'll find me}} »'' ; * A : couplet 3 ''« {{lang|en|Somewhere […] oh why can't I?}} »'' ; : {{lien web | url = https://www.youtube.com/watch?v=1HRa4X07jdE | titre = Judy Garland - Over The Rainbow (Subtitles) | site = YouTube | auteur = Overtherainbow | consulté le = 2020-12-17 }} Une mise en œuvre de la forme AABA couramment utilisée en jazz est la forme anatole (à le pas confondre avec la succession d'accords du même nom), en anglais ''{{lang|en|rythm changes}}'' car elle s'inspire du morceau ''{{lang|en|I Got the Rythm}}'' de George Gerschwin (''Girl Crazy'', 1930) : * A : {{Times New Roman|I–vi–ii–V}} (succession d'accords « anatole ») ; * B : {{Times New Roman|III<sup>7</sup>–VI<sup>7</sup>–II<sup>7</sup>–V<sup>7</sup>}} (les fondamentales forment une succession de quartes, donc parcourent le « cercle des quintes » à l'envers). Par exemple, ''I Got the Rythm'' étant en ''ré''♭ majeur, la forme est : * A : D♭ - B♭m - E♭m - A♭ * B : F7 - B♭7 - E♭7 - A♭7 === Exemples === ==== Début du Largo de la symphonie du Nouveau Monde ==== [[File:Largo nouveau monde 5 1res mesures.svg|vignette|Partition avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]] [[File:Largo nouveau monde 5 1res mesures.midi|vignette|Fichier son avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]] Nous avons reproduit ci-contre les cinq premières mesure du deuxième mouvement Largo de la symphonie « Du Nouveau Monde » (symphonie n<sup>o</sup> 9 d'Antonín Dvořák, 1893). Cliquez sur l'image pour l'agrandir. Vous pouvez écouter cette partie jouée par un orchestre symphonique : * {{lien web |url =https://www.youtube.com/watch?v=y2Nw9r-F_yQ?t=565 |titre = Dvorak Symphony No.9 "From the New World" Karajan 1966 |site=YouTube (Seokjin Yoon) |consulté le=2020-12-11 }} (à 9 min 25), par le Berliner Philharmoniker, dirigé par Herbert von Karajan (1966) ; * {{lien web |url = https://www.youtube.com/watch?v=ASlch7R1Zvo |titre=Dvořák: Symphony №9, "From The New World" - II - Largo |site=YouTube (diesillamusicae) |consulté le=2020-12-11 }} : Wiener Philharmoniker, dirigé par Herbert von Karajan (1985). {{clear}} Cette partie fait intervenir onze instruments monodiques (ne jouant qu'une note à la fois) : des vents (trois bois, sept cuivres) et une percussion. Certains de ces instruments sont transpositeurs (les notes sur la partition ne sont pas les notes entendues). Jouées ensemble, ces onze lignes mélodiques forment des accords. Pour étudier cette partition, nous réécrivons les parties des instruments transpositeurs en ''do'' et les parties en clef d’''ut'' en clef de ''fa''. Nous regroupons les parties en clef de ''fa'' d'un côté et les parties en clef de ''sol'' d'un autre. {{boîte déroulante|Résultat|contenu=[[File:Largo nouveau monde 5 1res mesures transpositeurs en do.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde, en do.]]}} Nous pouvons alors tout regrouper sous la forme d'un système de deux portées clef de ''fa'' et clef de ''sol'', comme une partition de piano. {{boîte déroulante|Résultat|contenu= [[Fichier:Largo nouveau monde 5 1res mesures accords.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords.]] }} [[Fichier:Largo nouveau monde 5 1res mesures accords.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords.]] {{clear}} Ensuite, nous ne gardons que la basse et les notes médium. Nous changeons éventuellement certaines notes d'octave afin de n'avoir que des superpositions de tierce ou de quinte (état fondamental des accords, en faisant ressortir les notes manquantes). {{boîte déroulante|Résultat|contenu= [[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords simplifiés.]] }} [[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords simplifiés.]] Vous pouvez écouter cette partie jouée par un quintuor de cuivres (trompette, bugle, cor, trombone, tuba), donc avec des accords de cinq notes : : {{lien web |url=https://www.youtube.com/watch?v=pWfe60nbvjA |titre = Largo from The New World Symphony by Dvorak |site=YouTube (The Chamberlain Brass) |consulté le=2020-12-11 }} : The American Academy of Arts & Letters in New York City (2017). Nous allons maintenant chiffrer les accords. Pour établir la basse chiffrée, il nous faut déterminer le parcours harmonique. Pour le premier accord, les tonalités les plus simples avec un ''sol'' dièse sont ''la'' majeur et ''fa'' dièse mineur ; comme le ''mi'' est bécarre, nous retenons ''la'' majeur, il s'agit donc d'un accord de quinte sur la dominante (les accords de dominante étant très utilisés, cela nous conforte dans notre choix). Puis nous avons un ''si'' bémol, nous pouvons être en ''fa'' majeur ou en ''ré'' mineur ; nous retenons ''fa'' majeur, c'est donc le renversement d'un accord sur le degré {{Times New Roman|II}}. Dans la deuxième mesure, nous revenons en ''la'' majeur, puis, avec un ''la'' et un ''ré'' bémols, nous sommes en ''la'' bémol majeur ; nous avons donc un accord de neuvième incomplet sur la sensible, ou un accord de onzième incomplet sur la dominante. Dans la troisième mesure, nous passons en ''ré'' majeur, avec un accord de dominante. Puis, nous arrivons dans la tonalité principale, avec le renversement d'un accord de dominante sans tierce suivi d'un accord de tonique. Nous avons donc une cadence parfaite, conclusion logique d'une phrase. La progression des accords est donc : {| class="wikitable" ! scope="row" | Tonalité | ''la'' M - ''fa'' M || ''la'' M - ''la''♭ M || ''ré'' M - ''ré''♭ M || ''ré''♭ M |- ! scope="row" | Accords | {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|II}}<sup>6</sup><sub>4</sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|“V”}}<sup>9</sup><sub><s>5</s></sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|V}}<sup>+4</sup> || {{Times New Roman|I}}<sup>5</sup> |} Dans le chiffrage jazz, nous avons donc : * une triade de ''mi'' majeur, E ; * une triade de ''sol'' majeur avec un ''ré'' en basse : G/D ; * à nouveau un E ; * un accord de ''sol'' neuvième diminué incomplet, avec un ''ré'' bémol en basse : G dim<sup>9</sup>/D♭ ; * un accord de ''la'' majeur, A ; * un accord de ''la'' bémol septième avec une ''sol'' bémol à la basse : A♭<sup>7</sup>/G♭ ; * la partie se conclue par un accord parfait de ''ré''♭ majeur, D♭. Soit une progression E - G/D | E - G dim<sup>9</sup>/D♭ | A - A♭<sup>7</sup>/G♭ | D♭. [[Fichier:Largo nouveau monde 5 1res mesures accords chiffres.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde en accords simplifiés.]] {{clear}} ==== Thème de Smoke on the Water ==== Le morceau ''Smoke on the Water'' du groupe Deep Purple (album ''Machine Head'', 1972) possède un célèbre thème, un riff ''({{lang|en|rythmic figure}})'', joué à la guitare sous forme d'accords de puissance ''({{lang|en|power chords}})'', c'est-à-dire des accords sans tierce. Le morceau est en tonalité de ''sol'' mineur naturel (donc avec un ''fa''♮) avec ajout de la note bleue (''{{lang|en|blue note}}'', quinte diminuée, ''ré''♭), et les accords composant le thème sont G<sup>5</sup>, B♭<sup>5</sup>, C<sup>5</sup> et D♭<sup>5</sup>, ce dernier accord étant l'accord sur la note bleue et pouvant être considéré comme une appoggiature (indiqué entre parenthèse ci-après). On a donc ''a priori'', sur les deux premières mesures, une progression {{Times New Roman|I-III-IV}} puis {{Times New Roman|I-III-(♭V)-IV}}. Durant la majeure partie du thème, la guitare basse tient la note ''sol'' en pédale. {{note|En jazz, la qualité « <sup>5</sup> » indique que l'on n'a que la quinte (et donc pas la tierce), contrairement à la notation de basse chiffrée.}} : {{lien web | url = https://www.dailymotion.com/video/x5ili04 | titre = Deep Purple — Smoke on the Water (Live at Montreux 2006) | auteur = Deep Purple | site = Dailymotion | date = 2016 | consulté le = 2020-12-31 }} Cependant, cette progression forme une mélodie, on peut donc plus la voir comme un contrepoint, la superposition de deux voies ayant un mouvement conjoint, joué par un seul instrument, la guitare, la voie 2 étant jouée une quarte juste en dessous de la voie 1 (la quarte juste descendante étant le renversement de la quinte juste ascendante) : * voie 1 (aigu) : | ''sol'' - ''si''♭ - ''do'' | ''sol'' - ''si''♭ - (''ré''♭) - ''do'' | ; * voie 2 (grave) : | ''ré'' - ''fa'' - ''sol'' | ''ré'' - ''fa'' - (''la''♭) - ''sol'' |. En se basant sur la basse (''sol'' en pédale), nous pouvons considérer que ces deux mesures sont accompagnées d'un accord de Gm<sup>7</sup> (''sol''-''si''♭-''ré''-''fa''), chaque accord de la mélodie comprenant à chaque fois au moins une note de cet accord à l'exception de l'appogiature. {| class="wikitable" |+ Mise en évidence des notes de l'accord Gm<sup>7</sup> |- ! scope="row" | Accords | G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> |- ! scope="row" | Voie 1 | '''''sol''''' || '''''si''♭''' || ''do'' |- ! scope="row" | Voie 2 | '''''ré''''' || '''''fa''''' || '''''sol''''' |- ! scope="row" | Basse | '''''sol''''' || '''''sol''''' || '''''sol''''' |} Sur les deux mesures suivantes, la basse varie et suit les accords de la guitare avec un retard sur le dernier accord : {| class="wikitable" |+ Voies sur les mesure 3-4 du thème |- ! scope="row" | Accords | G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> || B♭<sup>5</sup> || G<sup>5</sup> |- ! scope="row" | Voie 1 | ''sol'' || ''si''♭ || ''do'' || ''si''♭ || ''sol'' |- ! scope="row" | Voie 2 | ''ré'' || ''fa'' || ''sol'' || ''fa'' || ''ré'' |- ! scope="row" | Basse | ''sol'' || ''sol'' || ''do'' || ''si''♭ || ''si''♭-''sol'' |} Le couplet de cette chanson est aussi organisé sur une progression de quatre mesures, la guitare faisant des arpèges sur les accords G<sup>5</sup> (''sol''-''ré''-''sol'') et F<sup>5</sup> (''fa''-''do''-''fa'') : : | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-F<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | soit une progression {{Times New Roman|<nowiki>| I-I | I-I | I-VII | I-I |</nowiki>}}. Nous pouvons aussi harmoniser le riff du thème sur cette progression, avec un accord F (''fa''-''la''-''do'') ; nous pouvons aussi nous rappeler que l'accord sur le degré {{Times New Roman|VII}} est plus volontiers considéré comme un accord de septième de dominante {{Times New Roman|V<sup>7</sup>}}, soit ici un accord Dm<sup>7</sup> (''ré''-''fa''-''la''-''do''). On peut donc considérer la progression harmonique sur le thème : : | Gm-Gm | Gm-Gm | Gm-F ou Dm<sup>7</sup> | Gm-Gm |. Cette analyse permet de proposer une harmonisation enrichie du morceau, tout en se rappelant qu'une des forces du morceau initial est justement la simplicité de sa structure, qui fait ressortir la virtuosité des musiciens. Nous pouvons ainsi comparer la version album à la version concert avec orchestre ou à la version latino de Pat Boone. À l'inverse, le groupe Psychostrip, dans une version grunge, a remplacé les accords par une ligne mélodique : * le thème ne contient plus qu'une seule voie (la guitare ne joue pas des accords de puissance) ; * dans les mesures 9 et 10, la deuxième guitare joue en contrepoint de type mouvement inverse, qui est en fait la voie 2 jouée en miroir ; * l'arpège sur le couplet est remplacé par une ligne mélodique en ostinato sur une gamme blues. {| class="wikitable" |+ Contrepoint sur les mesures 9 et 10 |- ! scope="row" | Guitare 1 | ''sol'' ↗ ''si''♭ ↗ ''do'' |- ! scope="row" | Guitare 2 | ''sol'' ↘ ''fa'' ↘ ''ré'' |} * {{lien web | url = https://www.dailymotion.com/video/x5ik234 | titre = Deep Purple — Smoke on the Water (In Concert with the London Symphony Orchestra, 1999) | auteur = Deep Purple | site = Dailymotion | date = 2016 | consulté le = 2020-12-31 }} * {{lien web | url = https://www.youtube.com/watch?v=MtUuNzVROIg | titre = Pat Boone — Smoke on the Water (In a Metal Mood, No More Mr. Nice Guy, 1997) | auteur = Orrore a 33 Giri | site = YouTube | date = 2019-06-24 | consulté le = 2020-12-31 }} * {{lien web | url = https://www.youtube.com/watch?v=n7zLlZ8B0Bk | titre = Smoke on the Water (Heroes, 1993) | auteur = Psychostrip | site = YouTube | date = 2018-06-20 | consulté le = 2020-12-31 }} == Accords et improvisation == Nous avons vu précédemment (chapitre ''[[../Gammes et intervalles#Modes et improvisation|Gammes et intervalles &gt; Modes et improvisation]]'') que le choix d'un mode adapté permet d'improviser sur un accord. L'harmonisation des gammes permet, en inversant le processus, d'étendre notre palette : il suffit de repérer l'accord sur une harmonisaiton de gamme, et d'utiliser cette gamme-là, dans le mode correspondant du degré de l'accord (voir ci-dessus ''[[#Harmonisation par des accords de septième|Harmonisation par des accords de septième]]''). Par exemple, nous avons vu que l'accord sur le septième degré d'une gamme majeure était un accord demi-diminué ; nous savons donc que sur un accord demi-diminué, nous pouvons improviser sur le mode correspondant au septième degré, soit le mode de ''si'' (locrien). Un accord de septième de dominante étant commun aux deux tonalités homonymes (par exemple ''fa'' majeur et ''fa'' mineur pour un ''do''<sup>7</sup><sub>+</sub> / C<sup>7</sup>), nous pouvons utiliser le mode de ''sol'' de la gamme majeure (mixolydien) ou de la gamme mineure mineure (mode phrygien dominant, ou phrygien espagnol) pour improviser. Mais l'accord de septième de dominante est aussi l'accord au début d'une grille blues ; on peut donc improviser avec une gamme blues, même si la tierce est majeure dans l'accord et mineure dans la gamme. [[Fichier:Mode improvisation accords do complet.svg]] == Autres accords courants == [[fichier:Cluster cdefg.png|vignette|Agrégat ''do - ré - mi - fa - sol''.]] Nous avons vu précédemment l'harmonisation des tonalités majeures et mineures harmoniques par des triades et des accords de septième ; certains accords étant rarement utilisés (l'accord sur le degré {{Times New Roman|III}} et, pour les tonalités mineures harmoniques, l'accord sur la tonique), certains accords étant utilisés comme des accords sur un autre degré (les accords sur la sensible étant considérés comme des accords de dominante sans fondamentale). Dans l'absolu, on peut utiliser n'importe quelle combinaison de notes, jusqu'aux agrégats, ou ''{{lang|en|clusters}}'' (mot anglais signifiant « amas », « grappe ») : un ensemble de notes contigües, séparées par des intervalles de seconde. Dans la pratique, on reste souvent sur des accords composés de superpositions de tierces, sauf dans le cas de transitions (voir la section ''[[#Notes étrangères|Notes étrangère]]''). === En musique classique === On utilise parfois des accords dont les notes ne sont pas dans la tonalité (hors modulation). Il peut s'agir d'accords de passage, de notes étrangères, par exemple utilisant un chromatisme (mouvement conjoint par demi-tons). Outre les accords de passage, les autres accords que l'on rencontre couramment en musique classique sont les accords de neuvième, et les accords de onzième et treizième sur tonique. Ces accords sont simplement obtenus en continuant à empiler les tierces. Il n'y a pas d'accord d'ordre supérieur car la quinzième est deux octaves au-dessus de la fondamentale. Comme pour les accords de septième, on distingue les accords de neuvième de dominante et les accords de neuvième d'espèce. Dans le cas de la neuvième de dominante, il y a une différence entre les tonalités majeures et mineures : l'intervalle de neuvième est respectivement majeur et mineur. Les chiffrages des renversements peuvent donc différer. Comme pour les accords de septième de dominante, on considère que les accords de septième sur le degré {{Times New Roman|VI}} sont en fait des accords de neuvième de dominante sans fondamentale. Les accords de neuvième d'espèce sont en général préparés et résolus. Préparés : la neuvième étant une note dissonante (c'est à une octave près la seconde de la fondamentale), l'accord qui précède doit contenir cette note, mais dans un accord consonant ; la neuvième est donc commune avec l'accord précédent. Résolus : la dissonance est résolue en abaissant la neuvième par un mouvement conjoint. Par exemple, en tonalité de ''do'' majeur, si l'on veut utiliser un accord de neuvième d'espèce sur la tonique ''(do - mi - sol - si - ré)'', on peut utiliser avant un accord de dominante ''(sol - si - ré)'' en préparation puis un accord parfait sur le degré {{Times New Roman|IV}} ''(fa - la - do)'' en résolution ; nous avons donc sur la voie la plus aigüe la succession ''ré'' (consonant) - ''ré'' (dissonant) - ''do'' (consonant). On rencontre également parfois des accords de onzième et de treizième. On omet en général la tierce, car elle est dissonante avec la onzième. L'accord le plus fréquemment rencontré est l'accord sur la tonique : on considère alors que c'est un accord sur la dominante que l'on a enrichi « par le bas », en ajoutant une quinte inférieure. par exemple, dans la tonalité de ''do'' majeur, l'accord ''do - sol - si - ré - fa'' est considéré comme un accord de septième de dominante sur tonique, le degré étant noté « {{Times New Roman|V}}/{{Times New Roman|I}} ». De même pour l'accord ''do - sol - si - ré - fa - la'' qui est considéré comme un accord de neuvième de dominante sur tonique. === En jazz === En jazz, on utilise fréquemment l'accord de sixte à la place de l'accord de septième majeure sur la tonique. Par exemple, en ''do'' majeur, on utilise l'accord C<sup>6</sup> ''(do - mi - sol - la)'' à la place de C<sup>Δ</sup> ''(do - mi - sol - si)''. On peut noter que C<sup>6</sup> est un renversement de Am<sup>7</sup> et pourrait donc se noter Am<sup>7</sup>/C ; cependant, le fait de le noter C<sup>6</sup> indique que l'on a bien un accord sur la tonique qui s'inscrit dans la tonalité de ''do'' majeur (et non, par exemple, de ''la'' mineur naturelle) — par rapport à l'harmonie fonctionnelle, on remarquera que Am<sup>7</sup> a une fonction tonique, l'utilisation d'un renversement de Am<sup>7</sup> à la place d'un accord de C<sup>Δ</sup> est donc logique. Les accords de neuvième, onzième et treizième sont utilisés comme accords de septième enrichis. Le chiffrage suit les règles habituelles : on ajoute un « 9 », un « 11 » ou un « 13 » au chiffrage de l'accord de septième. On utilise également des accords dits « suspendus » : ce sont des accords de transition qui sont obtenus en prenant une triade majeure ou mineure et en remplaçant la tierce par la quarte juste (cas le plus fréquent) ou la seconde majeure. Plus particulièrement, lorsque l'on parle simplement « d'accord suspendu » sans plus de précision, cela désigne l'accord de neuvième avec une quarte suspendue, noté « 9sus4 » ou simplement « sus ». == L'harmonie tonale == L'harmonie tonale est un ensemble de règle assez strictes qui s'appliquent dans la musique savante européenne, de la période baroque à la période classique classique ({{pc|xiv}}<sup>e</sup>-{{pc|xviii}}<sup>e</sup> siècle). Certaines règles sont encore largement appliquées dans divers styles musicaux actuels, y compris populaire (rock, rap…), d'autres sont au contraire ignorées (par exemple, un enchaînement de plusieurs accords de même qualité forme un mouvement parallèle, ce qui est proscrit en harmonie tonale). De nos jours, on peut voir ces règles comme des règles « de bon goût », et leur application stricte comme une manière de composer « à la manière de ». Précédemment, nous avons vu la progression des accords. Ci-après, nous abordons aussi la manière dont les notes de l'accord sont réparties entre plusieurs voix, et comment on construit chaque voix. === Concepts fondamentaux === ; Consonance : Les intervalles sont considérés comme « plus ou moins consonants » : :* consonance parfaite : unisson, quinte et octave ; :* consonance mixte (parfaite dans certains contextes, imparfaite dans d'autres) : quarte ; :* consonance imparfaite : tierce et sixte ; :* dissonance : seconde et septième. ; Degrés : Certains degrés sont considérés comme « forts », « meilleurs », ce sont les « notes tonales » : {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (sous-dominante) et {{Times New Roman|V}} (dominante). [[Fichier:Mouvements harmoniques.svg|vignette|upright=0.75|Mouvements harmoniques.]] ; Mouvements : Le mouvement décrit la manière dont les voix évoluent les unes par rapport aux autres : :# Mouvement parallèle : les voix sont séparées par un intervalle constant. :# Mouvement oblique : une voix reste constante, c'est le bourdon ; l'autre monte ou descend. :# Mouvement contraire : une voix descend, l'autre monte. :# Échange de voix : les voix échangent de note ; les mélodies se croisent mais on a toujours le même intervalle harmonique. {{clear}} === Premières règles === ; Règle du plus court chemin : Quand on passe d'un accord à l'autre, la répartition des notes se fait de sorte que chaque voix fait le plus petit mouvement possible. Notamment : si les deux accords ont des notes en commun, alors les voix concernées gardent la même note. : Les deux voix les plus importantes sont la voix aigüe — soprano — et la voix la plus grave — basse. Ces deux voix sont relativement libres : la voix de soprano a la mélodie, la voix de basse fonde l'harmonie. La règle du plus court chemin s'applique surtout aux voix intermédiaires ; si l'on a des mouvements conjoints, ou du moins de petits intervalles — c'est le sens de la règle du plus court chemin —, alors les voix sont plus faciles à interpréter. Cette règle évite également que les voix n'empiètent l'une sur l'autre (voir la règle « éviter le croisement des voix »). ; Éviter les consonances parfaites consécutives :* Lorsque deux voix sont à l'unisson ou à l'octave, elles ne doivent pas garder le même intervalle, l'effet serait trop plat. :* Lorsque deux voix sont à la quarte ou à la quinte, elles ne doivent pas garder le même intervalle, car l'effet est trop dur. : Pour éviter cela, lorsque l'on part d'un intervalle juste, on a intérêt à pratiquer un mouvement contraire aux voix qui ne gardent pas la même note, ou au moins un mouvement direct : les voix vont dans le même sens, mais l'intervalle change. : Notez que même avec le mouvement contraire, on peut avoir des consonances parfaites consécutives, par exemple si une voix fait ''do'' aigu ↗ ''sol'' aigu et l'autre ''sol'' médium ↘ ''do'' grave. : L'interdiction des consonances parfaites consécutives n'a pas été toujours appliquée, le mouvement parallèle strict a d'ailleurs été le premier procédé utilisé dans la musique religieuse au {{pc|x}}<sup>e</sup> siècle. On peut par exemple utiliser des quintes parallèles pour donner un style médiéval au morceau. On peut également utiliser des octaves parallèles sur plusieurs notes afin de créer un effet de renforcement de la mélodie. : Par ailleurs, les consonances parfaites consécutives sont acceptées lorsqu'il s'agit d'une cadence (transition entre deux parties ou bien conclusion du morceau). ; Éviter le croisement des voix : Les voix sont organisées de la plus grave à la plus aigüe. Deux voix n'étant pas à l'unisson, celle qui est plus aigüe ne doit pas devenir la plus grave et ''vice versa''. ; Soigner la partie soprano : Comme c'est celle qu'on entend le mieux, c'est en général celle qui porte la mélodie principale. On lui applique des règles spécifiques : :# Si elle chante la sensible dans un accord de dominante ({{Times New Roman|V}}), alors elle doit monter à la tonique, c'est-à-dire que la note suivante sera la tonique située un demi-ton au dessus. :# Si l'on arrive à une quinte ou une octave entre les parties basse et soprano par un mouvement direct, alors sur la partie soprano, le mouvement doit être conjoint. On doit donc arriver à cette situation par des notes voisines au soprano. ; Préférer certains accords : Les deux degrés les plus importants sont la tonique ({{Times New Roman|I}}) et la dominante ({{Times New Roman|V}}), les accords correspondants ont donc une importance particulière. : À l'inverse, l'accord de sensible ({{Times New Roman|VII}}) n'est pas considéré comme ayant une fonction harmonique forte. On le considère comme un accord de dominante affaibli. En tonalité mineure, on évite également l'accord de médiante ({{Times New Roman|III}}). : Donc on utilise en priorité les accords de : :# {{Times New Roman|I}} et {{Times New Roman|V}}. :# Puis {{Times New Roman|II}}, {{Times New Roman|IV}}, {{Times New Roman|VI}} ; et {{Times New Roman|III}} en mode majeur. :# On évite {{Times New Roman|VII}} ; et {{Times New Roman|III}} en mode mineur. ; Préférer certains enchaînements : Les enchaînements d'accord peuvent être classés par ordre de préférence. Par ordre de préférence décroissante (du « meilleur » au « moins bon ») : :# Meilleurs enchaînements : quarte ascendante ou descendante. Notons que la quarte est le renversement de la quinte, on a donc des enchaînements stables et naturels, mais avec un intervalle plus court qu'un enchaînement de quintes. :# Bons enchaînements : tierce ascendante ou descendante. Les accords consécutifs ont deux notes en commun. :# Enchaînements médiocres : seconde ascendante ou descendante. Les accords sont voisins, mais ils n'ont aucune note en commun. On les utilise de préférence en mouvement ascendant, et on utilise surtout les enchaînements {{Times New Roman|IV}}-{{Times New Roman|V}}, {{Times New Roman|V}}-{{Times New Roman|VI}} et éventuellement {{Times New Roman|I}}-{{Times New Roman|II}}. :# Les autres enchaînements sont à éviter. : On peut atténuer l'effet d'un enchaînement médiocre en plaçant le second accord sur un temps faible ou bien en passant par un accord intermédiaire. [[Fichier:Progression Vplus4 I6.svg|thumb|Résolution d'un accord de triton (quarte sensible) vers l'accord de sixte de la tonique.]] ; La septième descend par mouvement conjoint : Dans un accord de septième de dominante, la septième — qui est donc le degré {{Times New Roman|IV}} — descend par mouvement conjoint — elle est donc suivie du degré {{Times New Roman|III}}. : Corolaire : un accord {{Times New Roman|V}}<sup>+4</sup> se résout par un accord {{Times New Roman|I}}<sup>6</sup> : on a bien un enchaînement {{Times New Roman|V}} → {{Times New Roman|I}}, et la 7{{e}} (degré {{Times New Roman|IV}}), qui est la basse de l'accord {{Times New Roman|V}}<sup>+4</sup>, descend d'un degré pour donner la basse de l'accord {{Times New Roman|I}}<sup>6</sup> (degré {{Times New Roman|III}}). {{clear}} [[Fichier:Progression I64 V7plus I5.svg|thumb|Accord de sixte et de quarte cadentiel.]] ; Un accord de sixte et quarte est un accord de passage : Le second renversement d'un accord parfait est soit une appoggiature, soit un accord de passage, soit un accord de broderie. : S'il s'agit de l'accord de tonique {{Times New Roman|I}}<sup>6</sup><sub>4</sub>, c'est « accord de sixte et quarte de cadence », l'appoggiature de l'accord de dominante de la cadence parfaite. {{clear}} Mais il faut appliquer ces règles avec discernement. Par exemple, la voix la plus aigüe est celle qui s'entend le mieux, c'est donc elle qui porte la mélodie principale. Il est important qu'elle reste la plus aigüe. La voix la plus grave porte l'harmonie, elle pose les accords, il est donc également important qu'elle reste la plus grave. Ceci a deux conséquences : # Ces deux voix extrêmes peuvent avoir des intervalles mélodiques importants et donc déroger à la règle du plus court chemin : la voix aigüe parce que la mélodie prime, la voix de basse parce que la progression d'accords prime. # Les croisements des voix intermédiaires sont moins critiques. Par ailleurs, si l'on applique strictement toutes les règles « meilleurs accords, meilleurs enchaînements », on produit un effet conventionnel, stéréotypé. Il est donc important d'utiliser les solutions « moins bonnes », « médiocres » pour apporter de la variété. Ajoutons que les renversements d'accords permettent d'avoir plus de souplesse : on reste sur le même accord, mais on enrichit la mélodie sur chaque voix. Le ''Bolero'' de Maurice Ravel (1928) brise un certain nombre de ces règles. Par exemple, de la mesure 39 à la mesure 59, la harpe joue des secondes. De la mesure 149 à la mesure 165, les piccolo jouent à la sixte, dans des mouvement strictement parallèle, ce qui donne d'ailleurs une sonorité étrange. À partir de la mesure 239, de nombreux instruments jouent en mouvement parallèles (piccolos, flûtes, hautbois, cor, clarinettes et violons). === Application === [[Fichier:Harmonisation possible de frere jacques exercice.svg|vignette|Exercice : harmoniser ''Frère Jacques''.]] Harmoniser ''Frère Jacques''. Nous considérons un morceau à quatre voix : basse, ténor, alto et soprano. La soprano chante la mélodie de ''Frère Jacques''. L'exercice consiste à proposer l'écriture des trois autres voix en respectant les règles énoncées ci-dessus. Pour simplifier, nous ajoutons les contraintes suivantes : * toutes les voix chantent des blanches ; * nous nous limitons aux accords de quinte (accords de trois sons composés d'une tierce et d'une quinte) sans avoir recours à leurs renversements (accords de sixte, accords de sixte et de quarte). Les notes à gauche de la portée indiquent la tessiture (ou ambitus), l'amplitude que peut chanter la voix. {{clear}} {{boîte déroulante/début|titre=Solution possible}} [[Fichier:Harmonisation possible de frere jacques solution.svg|vignette|Harmonisation possible de ''Frère Jacques'' (solution de l'exercice).]] Il n'y a pas qu'une solution possible. Le premier accord doit contenir un ''do''. Nous sommes manifestement en tonalité de ''do'' majeur, nous proposons de commencer par l'accord parfait de ''do'' majeur, I<sup>5</sup>. Le deuxième accord doit comporter un ''ré''. Si nous utilisons l'accord de quinte de ''ré'', nous allons créer une quinte parallèle. Nous pourrions utiliser un renversement, mais nous nous imposons de chercher un autre accord. Il peut s'agir de l'accord ''si''<sup>5</sup> ''(si-ré-fa)'' ou de l'accord de ''sol''<sup>5</sup> ''(sol-si-ré)''. La dernière solution permet d'utiliser l'accord de dominante qui est un accord important de la tonalité. La règle du plus court chemin imposerait le ''sol'' grave pour la partie de basse, mais cela est proche de la limite du chanteur, nous préférons passer au ''sol'' aigu, plus facile à chanter. Nous vérifions qu'il n'y a pas de quinte parallèle : l'intervalle ascendant ''do-sol'' (basse-alto) devient ''sol-si'' (3<sup>ce</sup>), l'intervalle descendant ''do-sol'' (soprano-alto) devient ''ré-si'' (3<sup>ce</sup>). De la même manière, pour le troisième accord, nous ne pouvons pas passer à un accord de ''la''<sup>5</sup> pour éviter une quinte parallèle. Nous avons le choix entre ''do''<sup>5</sup> ''(do-mi-sol)'' et ''mi''<sup>5</sup> ''(mi-sol-si)''. Nous préférons revenir à l'accord de fondamental, solution très stable (l'enchaînement {{Times New Roman|V}}-{{Times New Roman|I}} formant une cadence parfaite). Pour le quatrième accord, nous pourrions rester sur l'accord parfait de ''do'' mais cela planterait en quelque sorte la fin du morceau puisque l'on resterait sur la cadence parfaite ; or, nous connaissons le morceau et savons qu'il n'est pas fini. Nous choisissons l'accord de ''la''<sup>5</sup> qui est une sixte ascendante ({{Times New Roman|I}}-{{Times New Roman|VI}}). Nos aurions pu répartir les voix différemment. Par exemple : * alto : ''sol''-''si''-''sol''-''do'' ; * ténor : ''mi''-''ré''-''mi''-''mi''. {{boîte déroulante/fin}} [[Fichier:Harmonisation possible de frere jacques.midi|vignette|Fichier son correspondant.]] {{clear}} == Annexe == === Accords en musique classique === Un accord est un ensemble de notes jouées simultanément. Il peut s'agir : * de notes jouées par plusieurs instruments ; * de notes jouées par un même instrument : piano, clavecin, orgue, guitare, harpe (la plupart des instruments à clavier et des instruments à corde). Pour deux notes jouées simultanément, on parle d'intervalle « harmonique » (par opposition à l'intervalle « mélodique » qui concerne les notes jouées successivement). Les notes répétées à différentes octaves ne changent pas la nature de l'accord. La musique classique considère en général des empilements de tierces ; un accord de trois notes sera constitué de deux tierces successives, un accord de quatre notes de trois tierces… Lorsque tous les intervalles sont des intervalles impairs — tierces, quintes, septièmes, neuvièmes, onzièmes, treizièmes… — alors l'accord est dit « à l'état fondamental » (ou encore « primitif » ou « direct »). La note de la plus grave est appelée « fondamentale » de l'accord. Lorsque l'accord comporte un ou des intervalles pairs, l'accord est dit « renversé » ; la note la plus grave est appelée « basse ». De manière plus générale, l'accord est dit à l'état fondamental lorsque la basse est aussi la fondamentale. On a donc un état idéal de l'accord (état canonique) — un empilement strict de tierces — et l'état réel de l'accord — l'empilement des notes réellement jouées, avec d'éventuels redoublements, omissions et inversions ; et seule la basse indique si l'accord est à l'état fondamental ou renversé. Le chiffrage dit de « basse continue » ''({{lang|it|basso continuo}})'' désigne la représentation d'un accord sous la forme d'un ou plusieurs chiffres arabes et éventuellement d'un chiffre romain. ==== Accords de trois notes ==== En musique classique, les seuls accords considérés comme parfaitement consonants, c'est-à-dire sonnant agréablement à l'oreille, sont appelés « accords parfaits ». Si l'on prend une tonalité et un mode donné, alors l'accord construit par superposition es degrés I, III et V de cette gamme porte le nom de la gamme qui l'a généré. [[fichier:Accord do majeur chiffre.svg|vignette|upright=0.5|Accord parfait de ''do'' majeur chiffré.]] Par exemple : * « l'accord parfait de ''do'' majeur » est composé des notes ''do'', ''mi'' et ''sol'' ; * « l'accord parfait de ''la'' mineur » est composé des notes ''la'', ''do'' et ''mi''. Un accord parfait majeur est donc composé, en partant de la fondamentale, d'une tierce majeure et d'une quinte juste. Un accord parfait mineur est composé d'une tierce mineure et d'une quinte juste. L'accord parfait à l'état fondamental est appelé « accord de quinte » et est simplement chiffré « 5 » pour indiquer la quinte. On peut également commencer un accord sur sa deuxième ou sa troisième note, en faisant monter celle(s) qui précède(nt) à l'octave suivante. On parle alors de « renversement d'accord » ou d'accord « renversé ». [[Fichier:Accord do majeur renversements chiffre.svg|vignette|upright=0.75|Accord parfait de ''do'' majeur et ses renversements, chiffrés.]] Par exemple, * le premier renversement de l'accord parfait de ''do'' majeur est :<br /> ''mi'', ''sol'', ''do'' ; * le second renversement de l'accord parfait de do majeur est :<br /> ''sol'', ''do'', ''mi''. Les notes conservent leur nom de « fondamentale », « tierce » et « quinte » malgré le changement d'ordre. La note la plus grave est appelée « basse ». Dans le cas du premier renversement, le deuxième note est la tierce de la basse (la note la plus grave) et la troisième note est la sixte ; le chiffrage en chiffres arabes est donc « 6 » (puisque l'on omet la tierce) et l'accord est appelé « accord de sixte ». Pour le deuxième renversement, les intervalles sont la quarte et la sixte, le chiffrage est donc « 6-4 » et l'accord est appelé « accord de sixte et de quarte ». Dans tous les cas, on chiffre le degré on considérant la fondamentale, par exemple {{Times New Roman|I}} si l'accord est construit sur la tonique de la gamme. Les autres accords de trois notes que l'on rencontre sont : * l'accord de quinte diminuée, constitué d'une tierce mineure et d'une quinte diminuée ; lorsqu'il est construit sur le septième degré d'une gamme, on considère que c'est un accord de septième de dominante sans fondamentale (voir plus bas), le degré est donc indiqué « “{{Times New Roman|V}}” » (cinq entre guillemets) et non « {{Times New Roman|VII}} » ; * l'accord de quinte augmenté : il est composé d'une tierce majeure et qu'une quinte augmentée. Dans le tableau ci-dessous, * « m » désigne un intervalle mineur ; * « M » un intervalle majeur ou le mode majeur ; * « J » un intervalle juste ; * « d » un intervalle diminué ; * « A » un intervalle augmenté ; * « mh » le mode mineur harmonique ; * « ma » le mode mineur ascendant ; * « md » le mode mineur descendant. {| class="wikitable" |+ Accords de trois notes ! scope="col" rowspan="2" | Nom ! scope="col" rowspan="2" | 3<sup>ce</sup> ! scope="col" rowspan="2" | 5<sup>te</sup> ! scope="col" rowspan="2" | État fondamental ! scope="col" rowspan="2" | 1<sup>er</sup> renversement ! scope="col" rowspan="2" | 2<sup>nd</sup> renversement ! scope="col" colspan="4"| Construit sur les degrés |- ! scope="col" | M ! scope="col" | mh ! scope="col" | ma ! scope="col" | md |- | Accord parfait<br /> majeur || M || J | accord de quinte || accord de sixte || accord de<br />sixte et de quarte | {{Times New Roman|I, IV, V}} || {{Times New Roman|V, VI}} || {{Times New Roman|IV, V}} || {{Times New Roman|III, VI, VII}} |- | Accord parfait<br /> mineur || m || J | accord de quinte || accord de sixte || accord de<br />sixte et de quarte | {{Times New Roman|II, III, VI}} || {{Times New Roman|I, IV}} || {{Times New Roman|I, II}} || {{Times New Roman|I, IV, V}} |- | Accord de<br />quinte diminuée || m || d | accord de<br />quinte diminuée || accord de<br />sixte sensible<br />sans fondamentale || accord de triton<br />sans fondamentale | {{Times New Roman|VII (“V”)}} || {{Times New Roman|II, VII (“V”)}} || {{Times New Roman|VI, VII (“V”)}} || {{Times New Roman|II}} |- | Accord de<br />quinte augmentée || M || A | accord de<br />quinte augmentée || accord de sixte<br />et de tierce sensible || accord de sixte et de quarte<br />sur sensible | &nbsp; || {{Times New Roman|III}} || {{Times New Roman|III}} || &nbsp; |} ==== Accords de quatre notes ==== Les accords de quatre notes sont des accord composés de trois tierces superposées. La dernière note étant le septième degré de la gamme, on parle aussi d'accords de septième. Ces accords sont dissonants : ils contiennent un intervalle de septième (soit une octave montante suivie d'une seconde descendante). Ils laissent donc une impression de « tension ». Il existe sept différents types d'accords, ou « espèces ». Citons l'accord de septième de dominante, l'accord de septième mineure et l'accord de septième majeure. ===== L'accord de septième de dominante ===== [[Fichier:Accord 7e dominante do majeur renversements chiffre.svg|vignette|Accord de septième de dominante de ''do'' majeur et ses renversements, chiffrés.]] L'accord de septième de dominante est l'empilement de trois tierces à partir de la dominante de la gamme, c'est-à-dire du {{Times New Roman|V}}<sup>e</sup> degré. Par exemple, l'accord de septième de dominante de ''do'' majeur est l'accord ''sol''-''si''-''ré''-''fa'', et l'accord de septième de dominante de ''la'' mineur est ''mi''-''sol''♯-''si''-''ré''. L'accord de septième de dominante dont la fondamentale est ''do'' (''do''-''mi''-''sol''-''si''♭) appartient à la gamme de ''fa'' majeur. Que le mode soit majeur ou mineur, il est composé d'une tierce majeure, d'une quinte juste et d'une septième mineure (c'est un accord parfait majeur auquel on ajoute une septième mineure). C'est de loin l'accord de septième le plus utilisé ; il apparaît au {{pc|xvii}}<sup>e</sup> en musique classique. Dans son état fondamental, son chiffrage est {{Times New Roman|V 7/+}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}). Le signe plus indique la sensible. Son premier renversement est appelé « accord de quinte diminuée et sixte » et est noté {{Times New Roman|V 6/<s>5</s>}} (ou {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}). Son deuxième renversement est appelé « accord de sixte sensible », puisque la sixte de l'accord est la sensible de la gamme, et est noté {{Times New Roman|V +6}} (ou {{Times New Roman|V<sup>+6</sup>}}). Son troisième renversement est appelé « accord de quarte sensible » et est noté {{Times New Roman|V +4}} (ou {{Times New Roman|V<sup>+4</sup>}}). [[Fichier:Accord 7e dominante sans fondamentale do majeur renversements chiffre.svg|vignette|Accord de septième de dominante sans fondamentale de ''do'' majeur et ses renversements, chiffrés.]] On utilise aussi l'accord de septième de dominante sans fondamentale ; c'est alors un accord de trois notes. Dans son état fondamental, c'est un « accord de quinte diminuée » placé sur le {{Times New Roman|VII}}<sup>e</sup> degré (mais c'est bien un accord construit sur le {{Times New Roman|V}}<sup>e</sup> degré), noté {{Times New Roman|“V” <s>5</s>}} (ou {{Times New Roman|“V”<sup><s>5</s></sup>}}). Notez les guillemets qui indiquent que la fondamentale V est absente. Dans son premier renversement, c'est un « accord de sixte sensible sans fondamentale » noté {{Times New Roman|“V” +6/3}} (ou {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}}). Dans son second renversement, c'est un « accord de triton sans fondamentale » (puisque le premier intervalle est une quarte augmentée qui comporte trois tons) noté {{Times New Roman|“V” 6/+4}} (ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}). Notons qu'un accord de septième de dominante n'a pas toujours la dominante pour fondamentale : tout accord composé d'une tierce majeure, d'une quinte juste et d'une septième mineure est un accord de septième de dominante et est chiffré {{Times New Roman|<sup>7</sup><sub>+</sub>}}, quel que soit le degré sur lequel il est bâti (certaines notes peuvent avoir une altération accidentelle). ===== Les accords de septième d'espèce ===== Les autres accords de septièmes sont dits « d'espèce ». L'accord de septième mineure est l'accord de septième formé sur la fondamentale d'une gamme mineure ''naturelle''. Par exemple, l'accord de septième mineure de ''la'' est ''la''-''do''-''mi''-''sol''. Il est composé d'une tierce mineure, d'une quinte juste et d'une septième mineure (c'est un accord parfait mineur auquel on ajoute une septième mineure). L'accord de septième majeure est l'accord de septième formé sur la fondamentale d'une gamme majeure. Par exemple, L'accord de septième majeure de ''do'' est ''do''-''mi''-''sol''-''si''. Il est composé d'une tierce majeure, d'une quinte juste et d'une septième majeure (c'est un accord parfait majeur auquel on ajoute une septième majeure). ==== Utilisation du chiffrage ==== Le chiffrage est utilisé de deux manières. La première manière, c'est la notation de la basse continue. La basse continue est une technique d'improvisation utilisée dans le baroque pour l'accompagnement d'instruments solistes. Sur la partition, on indique en général la note de basse de l'accord et le chiffrage en chiffres arabes. La seconde manière, c'est pour l'analyse d'une partition. Le fait de chiffrer les accords permet de mieux en comprendre la structure. De manière générale, on peut retenir que : * le chiffrage « 5 » indique un accord parfait, superposition d'une tierce (majeure ou mineure) et d'une quinte juste ; * le chiffrage « 6 » indique le premier renversement d'un accord parfait ; * le chiffrage « 6/4 » indique le second renversement d'un accord parfait ; * chiffrage « 7/+ » indique un accord de septième de dominante ; * le signe « + » indique en général que la note de l'intervalle est la sensible ; * un intervalle barré désigne un intervalle diminué. [[fichier:Accords gamme do majeur la mineur.svg|class=transparent| center | Principaux accords construits sur les gammes de ''do'' majeur et de ''la'' mineur harmonique.]] === Notation « jazz » === En jazz et de manière générale en musique rock et populaire, la base d'un accord est la triade composée d'une tierce (majeure ou mineure) et d'une quinte juste. Pour désigner un accord, on utilise la note fondamentale, éventuellement désigné par une lettre dans le système anglo-saxon (A pour ''la'' etc.), suivi d'une qualité (comme « m », « + »…). Les renversements ne sont pas notés de manière particulière, ils sont notés comme les formes fondamentales. Dans les deux tableaux suivants, la fondamentale est notée X (remplace le C pour un accord de ''do'', le D pour un accord de ''ré''…). La construction des accords est décrite par la suite. [[Fichier:Arbre accords triades 5d5J5A.svg|vignette|upright=1.5|Formation des triades présentée sous forme d'arbre.]] {| class="wikitable" |+ Notation des principales triades |- | ! scope="col" | Tierce<br />mineure (3m) ! scope="col" | Tierce<br />majeure (3M) |- ! scope="row" | Quinte diminuée (5d) | X<sup>o</sup>, Xm<sup>♭5</sup>, X–<sup>♭5</sup> || |- ! scope="row" | Quinte juste (5J) | Xm, X– || X |- ! scope="row" | Quinte augmentée (5A) | || X+, X<sup>♯5</sup> |} [[Fichier:Triades do.svg|class=transparent|center|Triades de do.]] {| class="wikitable" |+ Notation des principaux accords de septième |- | colspan="2" | ! scope="col" | Tierce<br />mineure (3m) ! scope="col" | Tierce<br />majeure (3M) |- ! scope="row" rowspan="2" | Quinte<br />diminuée (5d) ! scope="row" | Septième diminuée (7d) | X<sup>o7</sup> || |- ! scope="row" | Septième mineure (7m) | Xm<sup>7(♭5)</sup>, X–<sup>7(♭5)</sup>, X<sup>Ø</sup> || |- ! scope="row" rowspan="3" | Quinte<br />juste (5J) ! scope="row" | Sixte majeure (6M) | Xm<sup>6</sup> || X<sup>6</sup> |- ! scope="row" | Septième mineure (7m) | Xm<sup>7</sup>, X–<sup>7</sup> || X<sup>7</sup> |- ! scope="row" | Septième majeure (7M) | Xm<sup>maj7</sup>, X–<sup>maj7</sup>, Xm<sup>Δ</sup>, X–<sup>Δ</sup> || X<sup>maj7</sup>, X<sup>Δ</sup> |- ! scope="row" rowspan="2" | Quinte<br />augmentée (5A) ! scope="row" | Septième mineure (7m) | || X+<sup>7</sup> |- ! scope="row" | Septième majeure (7M) | || X+<sup>maj7</sup> |} [[Fichier:Arbre accords septieme.svg|class=transparent|center|Formation des accords de septième présentée sous forme d'arbre.]] &nbsp; &nbsp; [[Fichier:Accords do septieme.svg|class=transparent|center|Accord de do septième.]] On notera que l'intervalle de sixte majeure est l'enharmonique de celui de septième diminuée (6M = 7d). [[File:Principaux accords do.svg|class=transparent|center|Principaux accords de do.]] ==== Triades ==== ; Accords fondés sur une tierce majeure * accord parfait majeur : pas de notation *: p. ex. « ''do'' » ou « C » pour l'accord parfait de ''do'' majeur (''do'' - ''mi'' - ''sol'') ; Accords fondés sur une tierce mineure * accord parfait mineur : « m », « min » ou « – » *: « ''do'' m », « ''do'' – », « Cm », « C– »… pour l'accord parfait de ''do'' mineur (''do'' - ''mi''♭ - ''sol'') ==== Triades modifiées ==== ; Accords fondés sur une tierce majeure * accord augmenté (la quinte est augmentée) : aug, +, ♯5 *: « ''do'' aug », « ''do'' + », « ''do''<sup>♯5</sup> » « Caug », « C+ » ou « C<sup>♯5</sup> » pour l'accord de ''do'' augmenté (''do'' - ''mi'' - ''sol''♯) : L'accord augmenté est un empilement de tierces majeures. Ainsi, un accord augmenté a deux notes communes avec deux autres accords augmentés : C+ (''do'' - ''mi'' - ''sol''♯) a deux notes communes avec A♭+ (''la''♭ - ''do'' - ''mi'') et avec E+ (''mi'' - ''sol''♯ - ''si''♯) ; et on remarque que ces trois accords sont en fait enharmoniques (avec les enharmonies ''la''♭ = ''sol''♯ et ''si''♯ = ''do''). En effet, l'octave comporte six tons (sous la forme de cinq tons et deux demi-tons), et une tierce majeure comporte deux tons, on arrive donc à l'octave en ajoutant une tierce majeure à la dernière note de l'accord. ; Accords fondés sur une tierce mineure * accord diminué (la quinte est diminuée) : dim, o, ♭5 *: « ''do'' dim », « ''do''<sup>o</sup> », « ''do''<sup>♭5</sup> », « Cdim », « C<sup>o</sup> » ou « C<sup>♭5</sup> » pour l'accord de ''do'' diminuné (''do'' - ''mi''♭ - ''sol''♭) : On remarque que la quinte diminuée est l'enharmonique de la quarte augmentée et est l'intervalle appelé « triton » (car composé de trois tons). ; Accords fondés sur une tierce majeure ou mineure * accord suspendu de seconde : la tierce est remplacée par une seconde majeure : sus2 *: « ''do''<sup>sus2</sup> » ou « C<sup>sus2</sup> » pour l'accord de ''do'' majeur suspendu de seconde (''do''-''ré''-''sol'') * accord suspendu de quarte : la tierce est remplacée par une quarte juste : sus4 *: « ''do''<sup>sus4</sup> » ou « C<sup>sus4</sup> » pour l'accord de ''do'' majeur suspendu de quarte (''do''-''fa''-''sol'') ==== Triades appauvries ==== ; Accords fondés sur une tierce majeure ou mineure * accord de puissance : la tierce est omise, l'accord n'est constitué que de la fondamentale et de la quinte juste : 5 *: « ''do''<sup>5</sup> », « C<sup>5</sup> » pour l'accord de puissance de ''do'' (''do'' - ''la'') {{note|Très utilisé dans les musiques rock, hard rock et heavy metal, il est souvent joué renversé (''la'' - ''do'') ou bien avec l'ajout de l'octave (''do'' - ''la'' - ''do'').}} ==== Triades enrichies ==== ; Accords fondés sur une tierce majeure * accord de septième (la 7<sup>e</sup> est mineure) : 7 *: « ''do''<sup>7</sup> », « C<sup>7</sup> » pour l'accord de ''do'' septième, appelé « accord de septième de dominante de ''fa'' majeur » en musique classique (''do'' - ''mi'' - ''sol'' - ''si''♭) * accord de septième majeure : Δ, 7M ou maj7 *: « ''do'' <sup>Δ</sup> », « ''do'' <sup>maj7</sup> », « C<sup>Δ</sup> », « C<sup>7M</sup> »… pour l'accord de ''do'' septième majeure (''do'' - ''mi'' - ''sol'' - ''si'') ; Accords fondés sur une tierce mineure * accord de mineur septième (la tierce et la 7<sup>e</sup> sont mineures) : m7, min7 ou –7 *: « ''do'' m<sup>7</sup> », « ''do'' –<sup>7</sup> », « Cm<sup>7</sup> », « C–<sup>7</sup> »… pour l'accord de ''do'' mineur septième, appelé « accord de septième de dominante de ''fa'' mineur » en musique classique (''do'' - ''mi''♭ - ''sol'' - ''si''♭) * accord mineure septième majeure : m7M, m7maj, mΔ, –7M, –7maj, –Δ *: « ''do'' m<sup>7M</sup> », « ''do'' m<sup>maj7</sup> », « ''do'' –<sup>Δ</sup> », « Cm<sup>7M</sup> », « Cm<sup>maj7</sup> », « C–<sup>Δ</sup> »… pour l'accord de ''do'' mineur septième majeure (''do'' - ''mi''♭ - ''sol'' - ''si'') * accord de septième diminué (la quinte et la septième sont diminuée) : dim 7 ou o7 *: « ''do'' dim<sup>7</sup> », « ''do''<sup>o7</sup> », « Cdim<sup>7</sup> » ou « C<sup>o7</sup> » pour l'accord de ''do'' septième diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si''♭) * accord demi-diminué (seule la quinte est diminuée, la septième est mineure) : Ø ou –7(♭5) *: « ''do''<sup>Ø</sup> », « ''do''<sup>7(♭5)</sup> », « C<sup>Ø</sup> » ou « C<sup>7♭5</sup> » pour l'accord de ''do'' demi-diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si'') === Construction pythagoricienne des accords === Nous avons vu au débuts que lorsque l'on joue deux notes en même temps, leurs vibrations se superposent. Certaines superpositions créent un phénomène de battement désagréable, c'est le cas des secondes. Dans le cas d'une tierce majeure, les fréquences des notes quadruple et quintuple d'une même base : les fréquences s'écrivent 4׃<sub>0</sub> et 5׃<sub>0</sub>. Cette superposition de vibrations est agréable à l'oreille. Nous avons également vu que dans le cas d'une quinte juste, les fréquences sont le double et le triple d'une même base, ou encore le quadruple et sextuple si l'on considère la moitié de cette base. Ainsi, dans un accord parfait majeur, les fréquences des fondamentales des notes sont dans un rapport 4, 5, 6. De même, dans le cas d'un accord parfait mineur, les proportions sont de 1/6, 1/5 et 1/4. {{voir|[[../Caractéristiques_et_notation_des_sons_musicaux#Construction_pythagoricienne_et_gamme_de_sept_tons|Caractéristiques et notation des sons musicaux &gt; Construction pythagoricienne et gamme de sept tons]]}} === Un peu de physique : interférences === Les sons sont des vibrations. Lorsque l'on émet deux sons ou plus simultanément, les vibrations se superposent, on parle en physique « d'interférences ». Le modèle le plus simple pour décrire une vibration est la [[w:fr:Fonction sinus|fonction sinus]] : la pression de l'air P varie en fonction du temps ''t'' (en secondes, s), et l'on a pour un son « pur » : : P(''t'') ≈ sin(2π⋅ƒ⋅''t'') où ƒ est la fréquence (en hertz, Hz) du son. Si l'on émet deux sons de fréquence respective ƒ<sub>1</sub> et ƒ<sub>2</sub>, alors la pression vaut : : P(''t'') ≈ sin(2π⋅ƒ<sub>1</sub>⋅''t'') + sin(2π⋅ƒ<sub>2</sub>⋅''t''). Nous avons ici une [[w:fr:Identité trigonométrique#Transformation_de_sommes_en_produits,_ou_antilinéarisation|identité trigonométrique]] dite « antilinéarisation » : : <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{f_1 + f_2}{2}t \right ) \cdot \sin \left ( 2\pi \frac{f_1 - f_2}{2}t \right ).</math> On peut étudier simplement deux situations simples. [[Fichier:Battements interferentiels.png|vignette|Deux sons de fréquences proches créent des battements : la superposition d'une fréquence et d'une enveloppe.]] La première, c'est quand les fréquences ƒ<sub>1</sub> et ƒ<sub>2</sub> sont très proches. Alors, la moyenne (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2 est très proche de ƒ<sub>1</sub> et ƒ<sub>2</sub> ; et la demie différence (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2 est très proche de zéro. On a donc une enveloppe de fréquence très faible, (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2, dans laquelle s'inscrit un son de fréquence moyenne, (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2. C'est cette enveloppe de fréquence très faible qui crée les battements, désagréables à l'oreille. Sur l'image ci-contre, le premier trait rouge montre un instant où les vibrations sont opposées ; elles s'annulent, le son s'éteint. Le second trait rouge montre un instant où les vibrations sont en phase : elle s'ajoutent, le son est au plus fort. {{clear}} La seconde, c'est lorsque les deux fréquences sont des multiples entiers d'une même fréquence fondamentale ƒ<sub>0</sub> : ƒ<sub>1</sub> = ''n''<sub>1</sub>⋅ƒ<sub>0</sub> et ƒ<sub>0</sub> = ''n''<sub>0</sub>⋅ƒ<sub>0</sub>. On a alors : : <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{n_1 + n_2}{2}f_0 \cdot t \right ) \cdot \sin \left ( 2\pi \frac{n_1 - n_2}{2}f_0 \cdot t \right ).</math> On multiplie donc deux fonctions qui ont des fréquences multiples de ƒ<sub>0</sub>. La différence minimale entre ''n''<sub>1</sub> et ''n''<sub>2</sub> vaut 1 ; on a donc une enveloppe dont la fréquence est au minimum la moitié de ƒ<sub>0</sub>, c'est-à-dire un son une octave en dessous de ƒ<sub>0</sub>. Donc, cette enveloppe ne crée pas d'effet de battement, ou plutôt, le battement est trop rapide pour être perçu comme tel. Dans cette enveloppe, on a une fonction sinus dont la fréquence est également un multiple de ƒ<sub>0</sub> ; l'enveloppe et la fonction qui y est inscrite ont donc de nombreux « points communs », d'où l'effet harmonieux. === Le tonnetz === [[File:Speculum musicae.png|thumb|right|225px|Euler, ''De harmoniæ veris principiis'', 1774, p. 350.]] En allemand, le terme ''Tonnetz'' (se prononce « tône-netz ») signifie « réseau tonal ». C'est une représentation graphique des notes qui a été imaginée par [[w:Leonhard Euler|Leonhard Euler]] en 1739. Cette représentation graphique peut aider à la mémorisation de certains concepts de l'harmonie. Cependant, son application est très limitée : elle ne concerne que l'intonation juste d'une part, et que les accords parfait des tonalités majeures et mineures naturelles d'autre part. La représentation contenant les douze notes de la musique savante occidentale, on peut bien sûr représenter d'autres objets, comme les accords de septième ou les accords diminués, mais la représentation graphique est alors compliquée et perd son intérêt pédagogique. On part d'une note, par exemple le ''do''. Si on progresse vers la droite, on monte d'une quinte juste, donc ''sol'' ; vers la gauche, on descend d'une quinte juste, donc ''fa''. Si on va vers le bas, on monte d'une tierce majeure, donc ''mi'' ; si on va vers le haut, on descend d'une tierce majeure, donc ''la''♭ ou ''sol''♯ fa — do — sol — ré | | | | la — mi — si — fa♯ | | | | do♯ — sol♯ — ré♯ — si♭ La figure forme donc un filet, un réseau. On voit que ce réseau « boucle » : si on descend depuis le ''do''♯, on monte d'une tierce majeure, on obtient un ''mi''♯ qui est l'enharmonique du ''fa'' qui est en haut de la colonne. Si on va vers la droite à partir du ''ré'', on obtient le ''la'' qui est au début de la ligne suivante. Si on ajoute des diagonales allant vers la droite et le haut « / », on met en évidence des tierces mineures : ''la'' - ''do'', ''mi'' - ''sol'', ''si'' - ''ré'', ''do''♯ - ''mi''… fa — do — sol — ré | / | / | / | la — mi — si — fa♯ | / | / | / | do♯ — sol♯ — ré♯ — si♭ Donc les liens représentent : * | : tierce majeure ; * — : quinte juste ; * / : tierce mineure. [[Fichier:Tonnetz carre accords fr.svg|thumb|Tonnetz avec les accords parfaits. Les notes sont en notation italienne et les accords en notation jazz.]] On met ainsi en évidence des triangles dont un côté est une quinte juste, un côté une tierce majeure et un côté une tierce mineure ; c'est-à-dire que les notes aux sommets du triangle forment un accord parfait majeur (par exemple ''do'' - ''mi'' - ''sol'') : <div style="font-family:courier; background-color:#fafafa"> fa&nbsp;&nbsp;—&nbsp;'''do&nbsp;&nbsp;&nbsp;—&nbsp;sol'''&nbsp;—&nbsp;ré<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;'''|&nbsp;&nbsp;&nbsp;/'''&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;|<br /> la&nbsp;&nbsp;—&nbsp;'''mi'''&nbsp;&nbsp;&nbsp;—&nbsp;si&nbsp;&nbsp;—&nbsp;fa♯<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;/&nbsp;&nbsp;|<br /> do♯&nbsp;—&nbsp;sol♯&nbsp;—&nbsp;ré♯&nbsp;—&nbsp;si♭ </div> ou un accord parfait mineur (''la'' - ''do'' - ''mi''). <div style="font-family:courier; background-color:#fafafa"> fa&nbsp;&nbsp;—&nbsp;'''do'''&nbsp;&nbsp;&nbsp;—&nbsp;sol&nbsp;—&nbsp;ré<br /> |&nbsp;&nbsp;'''/&nbsp;&nbsp;|'''&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;|<br /> '''la&nbsp;&nbsp;—&nbsp;mi'''&nbsp;&nbsp;&nbsp;—&nbsp;si&nbsp;&nbsp;—&nbsp;fa♯<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;/&nbsp;&nbsp;|<br /> do♯&nbsp;—&nbsp;sol♯&nbsp;—&nbsp;ré♯&nbsp;—&nbsp;si♭ </div> Un triangle représente donc un accord, et un sommet représente une note. Si on passe d'un triangle à un triangle voisin, alors on passe d'un accord à un autre accord, les deux accords ayant deux notes en commun. Ceci illustre la notion de « plus court chemin » en harmonie : si on passe d'un accord à un autre en gardant un côté commun, alors on a un mouvement conjoint sur une seule des trois voix. Par rapport à l'harmonie fonctionnelle : les accords sont contigus à leur fonction, par exemple en ''do'' majeur : * fonction de tonique ({{Times New Roman|I}}) : C, A– et E– sont contigus ; * fonction de sous-dominante ({{Times New Roman|IV}}) : F et D– sont contigus ; * fonction de dominante ({{Times New Roman|V}}) : G et B<sup>o</sup> sont contigus. On notera que les triangles d'un schéma ''tonnetz'' ne représentent que des accords parfaits. Pour représenter un accord de quinte diminuée (''si'' - ''ré'' - ''fa'') ou les accords de septième, en particulier l'accord de septième de dominante, il faut étendre le ''tonnetz'' et l'on obtient des figures différentes. Par ailleurs, il est adapté à ce que l'on appelle « l'intonation juste », puisque tous les intervalles sont idéaux. [[Fichier:Tonnetz carre accords etendu fr.svg|vignette|Tonnetz étendu.]] [[Fichier:Tonnetz carre do majeur accords fr.svg|vignette|Tonnetz de la tonalité de ''do'' majeur. La représentation de l'accord de quinte diminuée sur ''si'' (B<sup>o</sup>) est une ligne et non un triangle.]] [[Fichier:Tonnetz carre do mineur accords fr.svg|vignette|Tonnetz des tonalités de ''do'' mineur naturel (haut) et ''do'' mineur harmonique (bas).]] Si l'on étend un peu le réseau : ré♭ — la♭ — mi♭ — si♭ — fa | / | / | / | / | fa — do — sol — ré — la | / | / | / | / | la — mi — si — fa♯ — do♯ | / | / | / | / | do♯ — sol♯ — ré♯ — la♯ — mi♯ | / | / | / | / | mi♯ — do — sol — ré — la on peut donc trouver des chemins permettant de représenter les accords de septième de dominante (par exemple en ''do'' majeur, G<sup>7</sup>) fa / sol — ré | / si et des accords de quinte diminuée (en ''do'' majeur : B<sup>o</sup>) fa / ré / si Une gamme majeure ou mineure naturelle peut se représenter par un trapèze rectangle : ''do'' majeur fa — do — sol — ré | / la — mi — si et ''do'' mineur la♭ — mi♭ — si♭ / | fa — do — sol — ré En revanche, la représentation d'une tonalité nécessite d'étendre le réseau afin de pouvoir faire figurer tous les accords, deux notes sont représentées deux fois. La représentation des tonalités mineures harmoniques prend une forme biscornue, ce qui nuit à l'intérêt pédagogique de la représentation. [[Fichier:Neo-Riemannian Tonnetz.svg|vignette|upright=2|Tonnetz avec des triangles équilatéraux.]] On peut réorganiser le schéma en décalant les lignes, afin d'avoir des triangles équilatéraux. Sur la figure ci-contre (en notation anglo-saxonne) : * si on monte en allant vers la droite « / », on a une tierce mineure ; * si on descend en allant vers la droite « \ », on a une tierce majeure ; * les liens horizontaux « — » représentent toujours des quintes justes * les triangles pointe en haut sont des accords parfaits mineurs ; * les triangles pointe en bas sont des accords parfaits majeurs. On a alors les accords de septième de dominante F / G — D \ / B et de quinte diminuée F / D / B les tonalités majeures F — C — G — D \ / A — E — B et les tonalités mineures naturelles A♭ — E♭ — B♭ / \ F — C — G — D == Notes et références == {{références}} == Voir aussi == === Bibliographie === * {{ouvrage |prénom1=Francesco |nom1=Gasparini |titre=L'Armonico Pratico al Cimbalo |éditeur=Giusseppe Antonio Silvani |lieu=Bologne |année=1772 |lang=it |url=https://imslp.org/wiki/L'Armonico_Pratico_al_Cimbalo_(Gasparini%2C_Francesco)}} === Liens externes === {{wikipédia|Consonance (harmonie tonale)}} {{wikipédia|Disposition de l'accord}} {{wikisource|Petit Manuel d’harmonie}} * {{lien web | url = https://www.apprendrelesolfege.com/chiffrage-d-accords | titre = Chiffrage d'accords (classique) | site = Apprendrelesolfege.com | consulté le = 2020-12-03 }} * {{lien web | url = https://www.coursd-harmonie.fr/introduction/introduction2_le_chiffrage_d_accords.php | titre = Introduction II : Le chiffrage d'accords | site = Cours d'harmonie.fr | consulté le = 2021-12-14 }} * {{lien web | url=https://www.coursd-harmonie.fr/ | titre = Cours d'harmonie en ligne | auteur = Jean-Baptiste Voinet | site=coursd-harmonie.fr | consulté le = 2021-12-20 }} * {{lien web | url=http://e-harmonie.e-monsite.com/ | titre = Cours d'harmonie classique en ligne | auteur = Olivier Miquel | site=e-harmonie | consulté le = 2021-12-24 }} * {{lien web | url=https://fr.audiofanzine.com/theorie-musicale/editorial/dossiers/les-gammes-et-les-modes.html | titre = Les bases de l’harmonie | site = AudioFanzine | date = 2013-07-23 | consulté le = 2024-01-12 }} * {{lien web | url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/l-accord-de-tristan-dechiffre-et-desacralise-2481605 | titre = L’Accord de Tristan déchiffré et désacralisé ! | site = France Musique | date = 2025-10-22 | consulté le = 2025-10-23 }} * {{lien web | url=https://www.youtube.com/watch?v=X62SAnyfQYw | titre = Qu'est-ce que la basse continue dans la musique baroque ? | auteur = Clément Rochefort | site = YouTube/France Musique | date = 2022-05-02 | consulté le = 2026-08-04 }} ---- ''[[../Mélodie|Mélodie]]'' &lt; [[../|↑]] &gt; ''[[../Représentation musicale|Représentation musicale]]'' [[Catégorie:Formation musicale (livre)|Harmonie]] 7bsfzw3lbf9vntscfbxfxhgi4ytguv6 773905 773903 2026-10-02T08:19:10Z Cdang 1202 /* Consonance et dissonance */ image renversement 773905 wikitext text/x-wiki {{Bases de solfège}} <span style="font-size:25px;">6. Harmonie</span> L'harmonie désigne les notes jouées en même temps, soit plusieurs instruments jouant chacun une note, soit un instrument jouant un accord (instrument dit polyphonique). == Première approche == L'exemple le plus simple d'harmonie est sans doute la chanson en canon : c'est un chant polyphonique, c'est-à-dire à plusieurs voix, chaque voix chantant la même chose en décalé. Prenons par exemple ''Vent frais, vent du matin'' (la version originale est ''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609) : [[Fichier:Vent frais vent du matin.svg|class=transparent|center|Partition de ''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]] [[Fichier:Vent frais vent du matin.midi|vignette|''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]] nous voyons que les voix se superposent de manière « harmonieuse ». Les notes de chaque voix se correspondent point par point (avec un retard), c'est donc un type d'harmonie polyphonique appelé « contrepoint ». Considérons la première note de la mesure 6 pour chaque voix. Nous avons la superposition des notes ''ré''-''fa''-''la'' (du grave vers l'aigu) ; la superposition de notes jouées ou chantées ensembles s'appelle un accord. Cet accord ''ré''-''fa''-''la'' porte le nom « d'accord parfait de ''ré'' mineur » : * « ''ré'' » car la note fondamentale est un ''ré'' ; * « parfait » car il est l'association d'une tierce, ''ré''-''fa'', et d'une quinte juste, ''ré''-''la'' ; * « mineur » car le premier intervalle, ''ré''-''fa'', est une tierce mineure. Considérons maintenant un chant accompagné au piano. Le piano peut jouer plusieurs notes en même temps, il peut jouer des accords. [[Fichier:Au clair de le lune chant et piano.svg|class=transparent|center|Deux premières mesure d’Au clair de la lune.]] [[Fichier:Au clair de le lune chant et piano.midi|vignette|Deux premières mesure d’Au clair de la lune.]] L'accord, les notes à jouer simultanément, sont écrites « en colonne ». Lorsqu'on les énonce, on les lit de bas en haut mais le pianiste les joue en pressant les touches du clavier en même temps, de manière « plaquée ». Le premier accord est composé des notes ''do''-''mi''-''sol'' ; il est appelé « accord parfait de ''do'' majeur » car la note fondamentale est ''do'', qu'il est l'association d'une tierce et d'une quinte juste et que le premier intervalle, ''do''-''mi'', est une tierce majeure. Si l'on lit toutes les notes, de la plus grave à la plus aiguë, nous avons : ''do'' (à la clef de ''fa'', main gauche du piano) - ''do'' - ''mi'' - ''sol'' (main droite du piano) - ''do'' (chant) ; nous voyons que la note ''do'' est jouée plusieurs fois, cela ne change pas la qualité de l'accord. == Consonance et dissonance == [[Fichier:Seconde majeure septieme mineure.svg|vignette|Seconde majeure et son renversement, le septième mineure.]] Les notions de consonance et de dissonance sont culturelles et changent selon l'époque. Nous pouvons néanmoins noter que : * l'accord de seconde, et son renversement la septième, créent des battements, les notes « frottent », c'est un intervalle harmonique dissonant ; mais dans le cas de la septième, comme les notes sont éloignées, le frottement est moins perceptible ; * les accords d'unisson, de tierce, quarte et quinte sonnent agréablement à l'oreille, ils sont consonants. Dans la musique savante européenne, au début du Moyen-Âge, seuls les accords d'unisson, de quarte et de quinte étaient considérés comme consonants, d'où leur qualification de « juste ». La tierce, et son renversement la sixte, étaient perçues comme dissonantes. L'harmonie joue avec les consonances et les dissonances. Dans un premier temps, les harmonies dissonantes sont utilisées pour créer des tensions qui sont ensuite résolues, on utilise des successions « consonant-dissonant-consonant ». À force d'entendre des intervalles considérés comme dissonants, l'oreille s'habitue et certains finissent par être considérés comme consonants ; c'est ce qui est arrivé à la tierce et à la sixte à la fin du Moyen Âge avec le contrepoint, puis à la septième plus tardivement. Il faut ici aborder la notion d'harmonique des notes. [[File:Harmoniques de do.svg|thumb|Les six premières harmoniques de ''do''.]] Lorsque l'on joue une note, on entend d'autres notes plus aigües et plus faibles ; la note jouée est appelée la « fondamentale » et les notes plus aigües et plus faibles sont les « harmoniques ». C'est cette accumulation d'harmoniques qui donne la couleur au son, son timbre, qui fait qu'un piano ne sonne pas comme un violon. Par exemple, si l'on joue un ''do''<sup>1</sup><ref>Pour la notation des octaves, voir ''[[../Représentation_musicale#Désignation_des_octaves|Représentation musicale &gt; Désignation des octaves]]''.</ref> (fondamentale), on entend le ''do''<sup>2</sup> (une octave plus aigu), puis un ''sol''<sup>2</sup>, puis encore un ''do''<sup>3</sup> plus aigu, puis un ''mi''<sup>3</sup>, puis encore un ''sol''<sup>3</sup>, puis un ''si''♭<sup>3</sup>… Ainsi, puisque lorsque l'on joue un ''do'' on entend aussi un ''sol'' très léger, alors jouer un ''do'' et un ''sol'' simultanément n'est pas choquant. De même pour ''do'' et ''mi''. De là vient la notion de consonance. Le statut du ''si''♭ est plus ambigu. Il fait partie des harmoniques qui sonnent naturellement, mais il forme une seconde descendante avec le ''do'', intervalle dissonant. Par ailleurs, on remarque que le ''si''♭ ne fait pas partie de la gamme de ''do'' majeur, contrairement au ''sol'' et au ''mi''. Pour le jeu sur les dissonances, on peut écouter par exemple la ''Toccata'' en ''ré'' mineur, op. 11 de Sergueï Prokofiev (1912). : {{lien web |url=https://www.youtube.com/watch?v=AVpnr8dI_50 |titre=Yuja Wang Prokofiev Toccata |site=YouTube |date=2019-02-26 |consulté le=2021-12-19}} == Contrepoint == Dans le chant grégorien, la notion d'accord n'existe pas. L'harmonie provient de la superposition de plusieurs mélodies, notamment dans ce que l'on appelle le « contrepoint ». Le terme provient du latin ''« punctum contra punctum »'', littéralement « point par point », et désigne le fait que les notes de chaque voix se correspondent. L'exemple le plus connu de contrepoint est le canon, comme ''Frère Jacques'' ou bien ''Vent frais, vent du matin'' présenté ci-dessus : chaque note d'un couplet correspond à une note du couplet précédent. Certains morceaux sont bâtis sur une écriture « en miroir » : l'ordre des notes est inversé entre les deux voix, ou bien les intervalles sont inversés (« mouvement contraire » : une tierce montante sur une voix correspond à une tierce descendante sur l'autre). On peut également citer le « mouvement oblique » (une des voix, le bourdon, chante toujours la même note) et le mouvement parallèle (les deux voix chantent le même air mais transposé, l'une est plus aiguë que l'autre). Nous reproduisons ci-dessous le début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel. [[Fichier:Haendel Sonate en trio re mineur debut canon.svg | vignette | center | upright=2 | Début du second ''Allergo'' de la sonate en trio en ''ré'' mineur de Haendel.]] [[Fichier:Haendel Sonate en trio re mineur debut.midi | vignette | Début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.]] Nous avons mis en évidence la construction en canon avec des encadrés de couleur : sur les quatre premières mesures, nous voyons trois thèmes repris alternativement par une voix et par l'autre. Ce type de procédé est très courant dans la musique baroque. Les procédés du contrepoint s'appliquent également à la danse : * unisson : les danseurs et danseuses font les mêmes gestes en même temps ; * répétition : le fait de répéter une série de gestes, une « phrase dansante » ; * canon : les gestes sont faits avec un décalage régulier d'un danseur ou d'une danseuse à l'autre ; * cascade : forme de canon dans laquelle le décalage est très petit ; * contraste : deux danseur·euses, ou deux groupes, ont des gestuelles très différentes ; * accumulation : la gestuelle se complexifie par l'ajout d'éléments au fur et à mesure ; ou bien le nombre de danseur·euses augmente ; * dialogue : les gestes de danseur·euses ou de groupes se répondent ; * contre-point : la gestuelle d'un ou une danseuse se superpose à la gestuelle d'un groupe ; * lâcher-rattraper : les danseurs et danseuses alternent danse à l'unisson et gestuelles indépendantes. : {{lien web | url=https://www.youtube.com/watch?v=wgblAOzedFc | titre=Les procédés de composition en danse | auteur= Doisneau Sport TV | site=YouTube | date=2020-03-16 | consulté le=2021-01-21 }} {{...}} == Ostinato == L'ostinato, mot italien signifiant « obstiné », c'est le fait de jouer en boucle un passage. L'ostinato peut ainsi former une structure autour de laquelle peut se développer le morceau. Lorsque l'ostinato est joué par l'instrument de basse (basson, violoncelle, viole de gambe basse, flûte à bec basse, main gauche du piano…), on parle de basse obstinée. Les grilles d'accord en jazz sont des ostinatos (ou ostinati) joués par la section rythmique. Exemples : * {{lien web |url=https://www.youtube.com/watch?v=QZpGe5rNJkI&t=41s |titre=Epic Patty Cake Song (I'll Think Of You) |auteur= Kurt Hugo Schneider |site=YouTube |date=2014-08-12 |consulté le=2026-01-24}} : un ostinato de percussions corporelles ; * {{lien web |url=https://www.youtube.com/watch?v=Kv-37-nXC48 |titre=Pow Wow ''Le lion est mort ce soir'' Les Victoires de la Musique 1993 |auteur=Solomon Linda |date=2015-02-02 |consulté le=2026-01-24}} : dans ce chant ''a capella'' (sans accompagnement d'instrument), les voix de basse forment une basse obstinée, sur les syllabes « oëap » durant le couplet et « awimbowé » sur le refrain ; * {{lien web |url=https://www.youtube.com/watch?v=JvNQLJ1_HQ0 |titre=Pachelbel Canon in D Major - the original and best version. |auteur=Voices of Music |site=YouTube |date=2008-09-02 |consulté le=2026-01-24}} : le ''Canon de Pachelbel de ''ré'' mineur'' (''Canon per tre Violini e Basso'', Johann Pachelbel, vers 1680) est construit sur une basse obstinée de huit mesures. == Les accords en général == Initialement, on a des chants polyphoniques, des voix qui chantent chacune une mélodie, les mélodies se mêlant. On remarque que certaines superpositions de notes sonnent de manière plus ou moins agréables, consonantes ou dissonantes. On en vient alors à associer ces notes, c'est-à-dire à considérer dès le départ la superposition de ces notes et non pas la rencontre de ces notes au gré des mélodies. Ces groupes de notes superposées forment les accords. En Europe, cette notion apparaît vers le {{pc|xiv}}<sup>e</sup> siècle avec notamment la ''[[wikipedia:fr:Messe de Notre Dame|Messe de Notre Dame]]'' de Guillaume de Machaut (vers 1360-1365). La notion « d'accord parfait » est consacrée par [[wikipedia:fr:Jean-Philippe Rameau|Jean-Philippe Rameau]] dans son ''Traité de l'harmonie réduite à ses principes naturels'', publié en 1722. === Qu'est-ce qu'un accord ? === Un accord est un ensemble d'au minimum trois notes jouées en même temps. « Jouées » signifie qu'il faut qu'à un moment donné, elles sonnent en même temps, mais le début ou la fin des notes peut être à des instants différents. Considérons que l'on joue les notes ''do'', ''mi'' et ''sol'' en même temps. Cet accord s'appelle « accord de ''do'' majeur ». En musique classique, on lui adjoint l'adjectif « parfait » : « accord parfait de ''do'' majeur ». Nous représentons ci-dessous trois manière de faire l'accord : avec trois instruments jouant chacun une note : [[Fichier:Do majeur trois portees.svg|class=transparent|center|Accord de ''do'' majeur avec trois instruments différents.]] Avec un seul instrument jouant simultanément les trois notes : [[Fichier:Chord C.svg|class=transparent|center|Accord de ''do'' majeur joué par un seul instrument.]] L'accord tel qu'il est joué habituellement par une guitare d'accompagnement : [[Fichier:Do majeur guitare.svg|class=transparent|center|Accord de ''do'' majeur à la guitare.]] Pour ce dernier, nous représentons le diagramme indiquant la position des doigts sur le manche au dessus de la portée et la tablature en dessous. Ici, c'est au total six notes qui sont jouées : ''mi'' grave, ''do'' médium, ''mi'' médium, ''sol'' médium, ''do'' aigu, ''mi'' aigu. Mais il s'agit bien des trois notes ''do'', ''mi'' et ''sol'' jouées à des octaves différentes. Nous remarquons également que la note de basse (la note la plus grave), ''mi'', est différente de la note fondamentale (celle qui donne le nom à l'accord), ''do'' ; l'accord est dit « renversé » (voir plus loin). === Comment joue-t-on un accord ? === Les notes ne sont pas forcément jouées en même temps ; elles peuvent être « égrainées », jouée successivement, ce que l'on appelle un arpège. La partition ci-dessous montre six manières différentes de jouer un accord de ''la'' mineur à la guitare, plaqué puis arpégé. [[Fichier:La mineur differentes executions.svg|class=transparent|center|Différentes exécution de l'accord de do majeur à la guitare.]] [[Fichier:La mineur differentes executions midi.midi|vignette|Différentes exécution de l'accord de la mineur à la guitare.]] Vous pouvez écouter l'exécution de cette partition avec le lecteur ci-contre. Seuls les instruments polyphoniques peuvent jouer les accords plaqués : instruments à clavier (clavecin, orgue, piano, accordéon), les instruments à plusieurs cordes pincées (harpe, guitare ; violon, alto, violoncelle et contrebasse joués en pizzicati). Les instruments à corde frottés de la famille du violon peuvent jouer des notes par deux à l'archet mais pas plus du fait de la forme bombée du chevalet ; cependant, un mouvement rapide permet de jouer les quatre cordes de manière très rapprochée. Les instruments à percussion de type xylophone ou le tympanon permettent de jouer jusqu'à quatre notes simultanément en tenant deux baguettes (mailloches, maillets) par main. Tous les instruments peuvent jouer des arpèges même si, dans le cas des instruments monodiques, les notes ne continuent pas à sonner lorsque l'on passe à la note suivante. L'arpège peut être joué par l'instrument de basse (basson, violoncelle, contrebasse, guitare basse, pédalier de l'orgue…), notamment dans le cas d'une basse continue ou d'une ''{{lang|en|walking bass}}'' (« basse marchante » : la basse joue des noires, donnant ainsi l'impression qu'elle marche). En jazz, et spécifiquement au piano, on a recours au ''{{lang|en|voicing}}'' : on choisit la manière dont on organise les notes pour donner une couleur spécifique, ou bien pour créer une mélodie en enchaînant les accords. Il est fréquent de ne pas jouer toutes les notes : si on n'en garde que deux, ce sont la tierce et la septième, car ce sont celles qui caractérisent l'accord (selon que la tierce est mineure ou majeure, que la septième est majeure ou mineure), et la fondamentale est en général jouée par la contrebasse ou guitare basse. {{clear}} === Classes d'accord === [[Fichier:Intervalles harmoniques accords classes.svg|vignette|upright=1.5|Intervalles harmoniques dans les accords classés de trois, quatre et cinq notes.]] Un accord composé d'empilement de tierces est appelé « accord classé ». En musique tonale, c'est-à-dire la musique fondée sur les gammes majeures ou mineures (cas majoritaire en musique classique), on distingue trois classes d'accords : * les accords de trois notes, ou triades, ou accords de quinte ; * les accords de quatre notes, ou accords de septième ; * les accords de cinq notes, ou accords de neuvième. En empilant des tierces, si l'on part de la note fondamentale, on a donc de intervalles de tierce, quinte, septième et neuvième. En musique tonale, les accords avec d'autres intervalles (hors renversement, voir ci-après), typiquement seconde, quarte ou sixte, sont considérés comme des transitions entre deux accords classés. Ils sont appelés, selon leur utilisation, « accords à retard » (en anglais : ''{{lang|en|suspended chord}}'', accord suspendu) ou « appoggiature » (note « appuyée », étrangère à l'harmonie). Voir aussi plus loin la notion de note étrangère. === Renversements d'accords === [[File:Accord do majeur renversements.svg|thumb|Accord parfait de do majeur et ses renversements.]] [[Fichier:Progression dominante renverse parfait do majeur.svg|vignette|upright=0.6|Progression accord de dominante renversé → accord parfait en ''do'' majeur.]] Un accord classé est donc un empilement de tierces. Si l'on change l'ordre des notes, on a toujours le même accord mais il est fait avec d'autres intervalles harmoniques. Par exemple, l'accord parfait de ''do'' majeur dans son état fondamental, c'est-à-dire non renversé, s'écrit ''do'' - ''mi'' - ''sol''. Sa note fondamentale, ''do'', est aussi se note de basse. Si maintenant on prend le ''do'' de l'octave supérieure, l'accord devient ''mi - sol - do'' ; c'est l'empilement d'une tierce ''(mi - sol)'' et d'une quarte ''(sol - do)'', soit la superposition d'une tierce ''(mi - sol)'' et d'une sixième ''(mi - do)''. C'est le premier renversement de l'accord parfait de ''do'' majeur ; la fondamentale est toujours ''do'' mais la basse est ''mi''. Le second renversement est ''sol - do - mi''. L'utilisation de renversement peut faciliter l'exécution de la progression d'accord. Par exemple, en tonalité ''do'' majeur, si l'on veut passer de l'accord de dominante ''sol - si - ré'' à l'accord parfait ''do - mi - sol'', alors on peut utiliser le second renversement de l'accord de dominante : ''ré - sol - si'' → ''do - mi - sol''. Ainsi, la basse descend juste d'un ton ''(ré → do)'' et sur un piano, la main reste globalement dans la même position. Le renversement d'un accord permet également de respecter certaines règles de l'harmonie classique, notamment éviter que des voix se suivent strictement (« mouvement parallèle »), ce qui aurait un effet de platitude. De manière générale, la notion de renversement permet deux choses : * d'enrichir l'œuvre : pour créer une harmonie donnée (c'est-à-dire des sons sonnant bien ensemble), nous avons plus de souplesse, nous pouvons organiser ces notes comme nous le voulons selon les voix ; * de simplifier l'analyse : quelle que soit la manière dont sont organisées les notes, cela nous ramène à un même accord. {{citation bloc|Or il, y a plusieurs manières de jouer un accord, selon que l'on aborde par la première note qui le constitue, ''do mi sol'', la deuxième, ''mi sol do'', ou la troisième note, ''sol do mi''. Ce sont les renversements, [que Rameau] va classer en différentes combinaisons d'une seule matrice. Faisant cela, Rameau divise le nombre d'accords [de septième] par quatre. Il simplifie, il structure […].|{{ouvrage|prénom1=André |nom1=Manoukian |titre=Sur les routes de la musique |éditeur=Harper Collins |année=2021 |passage=54 |isbn=979-1-03391201-9}} }} {{clear}} [[File:Plusieurs realisation 1er renversement doM.svg|thumb|Plusieurs réalisation du premier renversement de l'accord de ''do'' majeur.]] Notez que # Les notes peuvent être répétées plusieurs fois, à différentes octaves. # Dans la notion de renversement, seule importe en fait la note de basse. Ainsi, les accords ''mi-sol-do'', ''mi-do-sol'', ''mi-do-mi-sol'', ''mi-sol-mi-do''… sont tous une déclinaison du premier renversement de ''do-mi-sol'' et ils seront abrégés de la même manière (''mi''<sup>6</sup> en musique classique ou C/E en musique populaire et jazz, voir plus bas). {{clear}} == Notation des accords de trois notes == Les accords de trois notes sont appelés « accords de quinte » en classique, et « triades » en jazz. [[Fichier:Progression dominante renverse parfait do majeur chiffrage.svg|vignette|upright=0.7|Chiffrage du second renversement d'un accord de ''sol'' majeur et d'un accord de ''do'' majeur : notation en musique populaire et jazz (haut) et notation de basse chiffrée (bas).]] Les accords sont construits de manière systématique. Nous pouvons donc les représenter de manière simplifiée. Cette notation simplifiée des accords est appelée « chiffrage ». Reprenons la progression d'accords ci-dessus : « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » dans la tonalité de ''do'' majeur. On utilise en général trois notations différentes : * en musique populaire, jazz, rock… un accord est désigné par sa note fondamentale ; ici donc, les accords sont notés « ''sol'' - ''do'' » ou, en notation anglo-saxonne, « G - C » ;<br /> comme le premier accord est renversé, on indique la note de basse après une barre, la progression d'accords est donc chiffrée '''« ''sol''/''ré'' - ''do'' »''' ou '''« G/D - C »''' ;<br /> il s'agit ici d'accords composés d'une tierce majeure et d'une quinte juste ; si les accords sont constitués d'intervalles différents, nous ajoutons un symbole après la note : « m » ou « – » si la tierce est mineure, « dim » ou « ° » si la quinte est diminuée ; * en musique classique, on utilise la notation de « basse chiffrée » (utilisée notamment pour noter la basse continue en musique baroque) : on indique la note de basse sur la portée et on lui adjoint l'intervalle de la fondamentale à la note la plus haute (donc ici respectivement 6 et 5, puisque ''sol''-''si'' est une sixte et ''do''-''sol'' est une quinte), étant sous-entendu que l'on a des empilements de tierce en dessous ; mais dans le cas du premier accord, le premier intervalle n'est pas une tierce, mais une quarte ''(ré''-''sol)'', on note donc '''« ''ré'' <sup>6</sup><sub>4</sub> - ''do'' <sup>5</sup> »'''<ref>quand on ne dispose pas de la notation en supérieur (exposant) et inférieur (indice), on utilise parfois une notation sous forme de fraction : ''sol'' 6/4 et ''do'' 5/.</ref> ; * lorsque l'on fait l'analyse d'un morceau, on s'attache à identifier la note fondamentale de l'accord (qui est différente de la basse dans le cas d'un renversement) ; on indique alors le degré de la fondamentale : '''« {{Times New Roman|V<sup>6</sup><sub>4</sub> - I<sup>5</sup>}} »'''. La notation de basse chiffrée permet de construire l'accord à la volée : * on joue la note indiquée (basse) ; * s'il n'y a pas de 2 ni de 4, on lui ajoute la tierce ; * on ajoute les intervalles indiqués par le chiffrage. La notation de musique jazz oblige à connaître la composition des différents accords, mais une fois que ceux-ci sont acquis, il n'y a pas besoin de reconstruire l'accord. La notation de basse chiffrée avec les chiffres romains n'est pas utilisée pour jouer, mais uniquement pour analyser ; Sur les partitions avec basse chiffrée, il y a simplement les chiffrages indiqués au-dessus de la partie de basse. Le chiffrage avec le degré en chiffres romains présente l'avantage d'être indépendant de la tonalité et donc de se concentrer sur la fonction de l'accord au sein de la tonalité. Par exemple, ci-dessous, nous pouvons parler de la progression d'accords « {{Times New Roman|V - I}} » de manière générale, cette notation étant valable quelle que soit la tonalité. [[File:Progression dominante renverse parfait do majeur chiffrage basse continue.svg|thumb|Chiffrage en notation basse chiffrée de la progression d'accords « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » en do majeur.]] {{note|En notation de base continue avec fondamentale en chiffres romains, la fondamentale est toujours indiquée ''sous'' la portée de la partie de basse. Les intervalles sont indiqués au-dessus de la portée de la partie de basse ; lorsque l'on fait une analyse, on peut ayssi les indiquer à côté du degré en chiffres romains, donc sous la portée de la basse.}} {{note|En notation rock, le 5 en exposant indique un accord incomplet avec uniquement la fondamentale et la quinte, un accord sans tierce appelé « accord de puissance » ou ''{{lang|en|power chord}}''. Par exemple, C<sup>5</sup> est l'accord ''do-sol''.}} {{clear}} [[Fichier:Accords parfait do majeur basse chiffree fondamental et renverse.svg|vignette|upright=2.5|Chiffrage de l'accord parfait de ''do'' majeur en basse chiffrée, à l'état fondamental et ses renversements.]] Concernant les accords parfaits en notation de basse chiffrée : * un accord parfait à l'état fondamental est chiffré « <sup>5</sup> » ; on l'appelle « accord de quinte » ; * le premier renversement est chiffré « <sup>6</sup> » (la tierce est implicite) ; on l'appelle « accord de sixte » ; * le second renversement est noté « <sup>6</sup><sub>4</sub> » ; on l'appelle « accord de sixte et de quarte » (ou bien « de quarte et de sixte »). Par exemple, pour l'accord parfait de ''do'' majeur : * l'état fondamental ''do''-''mi''-''sol'' est noté ''do''<sup>5</sup> ; * le premier renversement ''mi''-''sol''-''do'' est noté ''mi''<sup>6</sup> ; * le second renversement ''sol''-''do''-''mi'' est noté ''sol''<sup>6</sup><sub>4</sub>. Il y a une exception : l'accord construit sur la sensible (7{{e}} degré) contient une quinte diminuée et non une quinte juste. Le chiffrage est donc différent : * l'état fondamental ''si''-''ré''-''fa'' est noté ''si''<sup><s>5</s></sup> (cinq barré), « accord de quinte diminuée » ; * le premier renversement ''ré''-''fa''-''si'' est noté ''ré''<sup>+6</sup><sub>3</sub>, « accord de sixte sensible et tierce » ; * le second renversement ''fa''-''si''-''ré'' est noté ''fa''<sup>6</sup><sub>+4</sub>, « accord de sixte et quarte sensible ». Par ailleurs, on ne considère pas qu'il est fondé sur la sensible, mais sur la dominante. Si certains indiquent le degré {{Times New Roman|VII}}, d'autres indiquent le degré {{Times New Roman|V}} en mettant des guillemets, « {{Times New Roman|“V”}} ». Donc selon l'état, le chiffrage est {{Times New Roman|“V”<sup><s>5</s></sup>}}, {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}} ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}. En notation jazz, on ajoute « dim », « <sup>o</sup> » ou bien « <sup>♭5</sup> » au chiffrage, ici : B dim, B<sup>o</sup> ou B<sup>♭5</sup> pour l'état fondamental. Pour les renversements : B dim/D et B dim/F ; ou bien B<sup>o</sup>/D et B<sup>o</sup>/F ; ou bien B<sup>♭5</sup>/D et B<sup>♭5</sup>/F. {{clear}} [[Fichier:Accords basse chiffree basse do fondamental et renverses.svg|vignette|upright=2|Basse chiffrée : accords de quinte, de sixte et de sixte et de quarte ayant pour basse ''do''.]] Et concernant les accords ayant pour basse ''do'' en tonalité de ''do'' majeur : * l'accord ''do''<sup>5</sup> est un accord à l'état fondamental, c'est donc l'accord ''do''-''mi''-''sol'' (sa fondamentale est ''do'') ; * l'accord ''do''<sup>6</sup> est le premier renversement d'un accord, c'est donc l'accord ''do''-''mi''-''la'' (sa fondamentale est ''la'') ; * l'accord ''do''<sup>6</sup><sub>4</sub> est le second renversement d'un accord, c'est donc l'accord ''do''-''fa''-''la'' (sa fondamentale est ''fa''). {{clear}} == Notes étrangères == La musique européenne s'appuie essentiellement sur des accords parfaits, c'est-à-dire fondés sur une tierce majeure ou mineure, et une quinte juste. Il arrive fréquemment qu'un accord ne soit pas un accord parfait. Les notes qui font partie de l'accord parfait sont appelées « notes naturelles » et la note qui n'en fait pas partie est appelée « note étrangère ». Il existe plusieurs types de notes étrangères : * anticipation : la note étrangère est une note naturelle de l'accord suivant ; * appogiature : note d'ornementation qui se résout par mouvement conjoint, c'est-à-dire qu'elle est suivie par une note située juste au-dessus ou en dessous (seconde ascendante ou descendante) qui est, elle, une note naturelle ; * broderie : on part d'une note naturelle, on monte ou on descend d'une seconde, puis on revient sur la note naturelle ; * double broderie : on part d'une note naturelle, on joue la note du dessus puis la note du dessous avant de revenir à la note naturelle ; ou bien on joue la note du dessous puis la note du dessus ; * échappée : note étrangère n'appartenant à aucune des autres catégories ; * note de passage : mouvement conjoint allant d'une note naturelle d'un accord à une note naturelle de l'accord suivant ; * pédale : la note de basse reste la même pendant plusieurs accords successifs ; * retard : la note étrangère est une note naturelle de l'accord précédent. Les notes étrangères ne sont pas chiffrées. [[File:Notes etrangeres accords.svg|center|Différents types de notes étrangères.]] {{note|Les anglophones distinguent deux types de retard : la ''{{lang|en|suspension}}'' est résolue vers le haut (le mouvement est ascendant), le ''{{lang|en|retardation}}'' est résolu vers le bas (le mouvement est descendant).}} == Principaux accords == Les trois principaux accords sont : * l'accord parfait majeur : il est construit sur les degrés {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (médiante) et {{Times New Roman|V}} (dominante) d'une gamme majeure ; il est noté {{Times New Roman|I}}<sup>5</sup>, {{Times New Roman|IV}}<sup>5</sup>, {{Times New Roman|V}}<sup>5</sup> ; * l'accord parfait mineur : il est construit sur les degrés {{Times New Roman|I}} (tonique) et {{Times New Roman|IV}} (sous-tonique) d'une gamme mineure harmonique ; il est également noté {{Times New Roman|I}}<sup>5</sup> et {{Times New Roman|IV}}<sup>5</sup>, les anglo-saxons le notent {{Times New Roman|i}}<sup>5</sup> et {{Times New Roman|iv}}<sup>5</sup> (la minuscule indiquant le caractère mineur) ; * l'accord de septième de dominante : il est construit sur le degré {{Times New Roman|V}} (dominante) d'une gamme majeure ou mineure harmonique ; il est noté {{Times New Roman|V}}<sup>7</sup><sub>+</sub>. On peut trouver ces trois accords sur d'autres degrés, et il existe d'autre types d'accords. Nous verrons cela plus loin. {| class="wikitable" |+ Constitution des principaux accords — Dénomination classique |- ! scope="col" | Accord ! scope="col" | 1<sup>er</sup> intervalle ! scope="col" | 2<sup>e</sup> intervalle ! scope="col" | 3<sup>e</sup> intervalle |- ! scope="row" | Accord parfait majeur | tierce majeure (3M) || quinte juste (5J) || — |- ! scope="row" | Accord parfait mineur | tierce mineure (3m) || quinte juste (5J) || — |- ! scope="row" | Accord de septième de dominante | tierce majeure (3M) || quinte juste (5J) || septième mineure (7m) |} {| class="wikitable" |+ Constitution des principaux accords — Dénomination jazz |- ! scope="col" | Accord ! scope="col" | 1<sup>er</sup> intervalle ! scope="col" | 2<sup>e</sup> intervalle ! scope="col" | 3<sup>e</sup> intervalle |- ! scope="row" | Triade majeure | tierce majeure (3M) || quinte juste (5J) || — |- ! scope="row" | Triade mineure | tierce mineure (3m) || quinte juste (5J) || — |- ! scope="row" | Accord de septième | tierce majeure (3M) || quinte juste (5J) || septième mineure (7m) |} {| border="0" |- | [[Fichier:Accord do majeur arpege puis plaque.midi | Accord parfait de ''do'' majeur (C).]] || [[Fichier:Accord do mineur arpege puis plaque.midi | Accord parfait de ''do'' mineur (Cm).]] || [[Fichier:Accord do septieme arpege puis plaque.midi | Accord de septième de dominante de ''fa'' majeur (C<sup>7</sup>).]] |- | Accord parfait<br /> de ''do'' majeur (C). || Accord parfait<br /> de ''do'' mineur (Cm). || Accord de septième de dominante<br /> de ''fa'' majeur (C<sup>7</sup>). |} '''Rappel :''' * la tierce mineure est composée d'un ton et demi (1 t ½) ; * la tierce majeur est composée de deux tons (2 t) ; * la quinte juste a la même altération que la fondamentale, sauf lorsque la fondamentale est ''si'' (la quinte juste est alors ''fa''♯) ; * la septième mineure est le renversement de la seconde majeure (1 t). [[File:Renversements accords pft fa maj basse chiffree.svg|thumb|Renversements de l'accord parfait de ''fa'' majeur, et la notation de basse chiffrée.]] [[File:Renversements accord sept de dom fa maj basse chiffree.svg|thumb|Renversements de l'accord de septième de dominante de ''fa'' majeur, et la notation de basse chiffrée.]] {| class="wikitable" |+ Notation des principaux accords en musique classique |- ! scope="col" | Accord ! scope="col" | État<br /> fondamental ! scope="col" | Premier<br /> renversement ! scope="col" | Deuxième<br /> renversement ! scope="col" | Troisième<br /> renversement |- ! scope="row" | Accord parfait | {{Times New Roman|I<sup>5</sup>}}<br/> acc. de quinte || {{Times New Roman|I<sup>6</sup>}}<br :> acc. de sixte || {{Times New Roman|I<sup>6</sup><sub>4</sub>}}<br /> acc. de quarte et de sixte || — |- ! scope="row" | Accord de septième<br /> de dominante | {{Times New Roman|V<sup>7</sup><sub>+</sub>}}<br /> acc.de septième de dominante || {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}<br />acc. de sixte et quinte diminuée || {{Times New Roman|V<sup>+6</sup>}}<br />acc. de sixte sensible || {{Times New Roman|V<sup>+4</sup>}}<br />acc. de quarte sensible<br />acc. de triton |} {| class="wikitable" |+ Notation des principaux accords en jazz |- ! scope="col" | Accord ! scope="col" | Chiffrage ! scope="col" | Renversements |- ! scope="row" | Triade majeure | X | rowspan="3" | Les renversements se notent en mettant la basse après une barre de fraction, par exemple pour la triade de ''do'' majeur : * état fondamental : C ; * premier renversement : C/E ; * second renversement : C/G. |- ! scope="row" | Triade mineure | Xm, X– |- ! scope="row" | Septième | X<sup>7</sup> |} {{clear}} Dans le cas d'un accord de septième de dominante, le nom de l'accord change selon que l'on est en musique classique ou en jazz : en musique classique, on donne le nom de la tonalité alors qu'en jazz, on donne le nom de la fondamentale. Ainsi, l'accord appelé « septième de dominante de ''do'' majeur » en musique classique, est appelé « ''sol'' sept » (G<sup>7</sup>) en jazz : la dominante (degré {{Times New Roman|V}}, dominante) de la tonalité de ''do'' majeur est la note ''sol''. Comment appelle-t-on en musique classique l'accord appelé « ''do'' sept » (C<sup>7</sup>) en jazz ? Les tonalités dont le ''do'' est la dominante sont les tonalités de ''fa'' majeur (''si''♭ à la clef) et de ''fa'' mineur harmonique (''si''♭, ''mi''♭, ''la''♭ et ''ré''♭ à la clef et ''mi''♮ accidentel). Il s'agit donc de l'accord de septième de dominante des tonalités de ''fa'' majeur et ''fa'' mineur harmonique. {| class="wikitable" |+ Accords fréquents pour quelques la tonalités majeures ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Accord parfait<br />{{Times New Roman|I<sup>5</sup>}} ! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}} |- |''Do'' majeur || || C<br />''do-mi-sol'' || G7<br />''sol-si-ré-fa'' |- |''Sol'' majeur || ''fa''♯ || G<br />''sol-si-ré'' || D7<br />''ré-fa''♯''-la-do'' |- |''Ré'' majeur || ''fa''♯, ''do''♯ || D<br />''ré-fa''♯''-la'' || A7<br />''la-do''♯''-mi-sol'' |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A<br />''la-do''♯''-mi'' || E7<br />''mi-sol''♯''-si-ré'' |- | ''Fa'' majeur || ''si''♭ || F<br />''fa-la-do'' || C7<br />''do-mi-sol-si''♭ |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭<br />''si''♭''-ré-fa'' || F7<br />''fa-la-do-mi''♭ |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭<br />''mi''♭''-sol-si''♭ || B♭7<br />''si''♭''-ré-fa-la''♭ |} {| class="wikitable" |+ Accords fréquents pour quelques la tonalités mineures harmoniques ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Accord parfait<br />{{Times New Roman|i<sup>5</sup>}} ! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}} |- |''La'' mineur<br />harmonique || || Am, A–<br />''la-do-mi'' || E7<br />''mi-sol''♯''-si-ré'' |- |''Mi'' mineur<br />harmonique || ''fa''♯ || Em, E–<br />''mi-sol-si'' || B7<br />''si-ré''♯''-fa''♯''-la'' |- |''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || Bm, B–<br />''si-ré-fa''♯ || F♯7<br />''fa''♯''la''♯''-do''♯''-mi'' |- |''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || F♯m, F♯–<br />''fa''♯''-la-do''♯ || C♯7<br />''do''♯''-mi''♯''-sol''♯''-si'' |- | ''Ré'' mineur<br />harmonique || ''si''♭ || Dm, D–<br />''ré-fa-la'' || A7<br />''la-do''♯''-mi-sol'' |- | ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || Gm, G–<br />''sol-si''♭''-ré'' || D7<br />''ré-fa''♯''-la-do'' |- | ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || Cm, C–<br />''do-mi''♭''-sol'' || G7<br />''sol-si''♮''-ré-fa'' |} {{clear}} == Accords sur les degrés d'une gamme == === Harmonisation d'une gamme === [[Fichier:Accord trois notes gamme do majeur chiffre.svg|vignette|upright=1.2|Accords de trois note sur la gamme de ''do'' majeur, chiffrés.]] On peut ainsi construire une triade par degré d'une gamme. Pour une gamme majeure, les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|V<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|II<sup>5</sup>}}, {{Times New Roman|III<sup>5</sup>}}, {{Times New Roman|VI<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|ii<sup>5</sup>}}, {{Times New Roman|iii<sup>5</sup>}}, {{Times New Roman|vi<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}. Les accords ont tous une quinte juste à l'exception de l'accord {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} qui a une quinte diminuée, raison pour laquelle le « 5 » est barré. C'est un accord dit « de quinte diminuée ». En jazz, l'accord diminué est noté « dim », « ° », « m<sup>♭5</sup> » ou « <sup>–♭5</sup> ». Nous avons donc trois types d'accords (dans la notation jazz) : X (triade majeure), Xm (triade mineure) et X° (triade diminuée), la lettre X remplaçant le nom de la note fondamentale. {{clear}} [[Fichier:Accord trois notes gamme la mineur chiffre.svg|vignette|upright=1.2|Accords de trois notes sur une gamme de ''la'' mineur harmonique, chiffrés.]] Pour une gamme mineure harmonique, les accords {{Times New Roman|III<sup>+5</sup>}}, {{Times New Roman|V<sup>♯</sup>}} et {{Times New Roman|VI<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|II<sup><s>5</s></sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|i<sup>5</sup>}}, {{Times New Roman|ii<sup><s>5</s></sup>}}, {{Times New Roman|iv<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}. Les accords {{Times New Roman|ii<sup><s>5</s></sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} ont une quinte diminuée ; ce sont des accords dits « de quinte diminuée ». L'accord {{Times New Roman|III<sup>+5</sup>}} a une quinte augmentée ; le signe « plus » indique que la note de cinquième, le ''sol'' dièse, est la sensible. En jazz, l'accord est noté « aug » ou « <sup>+</sup> ». Les autres accords ont une quinte juste. Aux trois accords générés par une gamme majeure (X, Xm et X°), nous voyons ici apparaître un quatrième type d'accord : la triade augmentée X<sup>+</sup>. Nous remarquons que des gammes ont des accords communs. Par exemple, l'accord {{Times New Roman|ii<sup>5</sup>}} de ''do'' majeur est identique à l'accord {{Times New Roman|iv<sup>5</sup>}} de ''la'' mineur (il s'agit de l'accord Dm). Quel que soit le mode, les accords construits sur la sensible (accord de quinte diminuée) sont rarement utilisés. S'ils le sont, c'est en tant qu'accord de septième de dominante sans fondamentale (voir ci-après). C'est la raison pour laquelle le chiffrage indique le degré {{Times New Roman|V}} entre guillemets, et non pas le degré {{Times New Roman|VII}} (mais pour des raisons de clarté, nous l'indiquons entre parenthèses au début). En mode mineur, l'accord de quinte augmentée {{Times New Roman|iii<sup>+5</sup>}} est très peu utilisé (voir plus loin ''[[#Progression_d'accords|Progression d'accords]]''). C'est un accord considéré comme dissonant. On voit que : * un accord parfait majeur peut appartenir à cinq gammes différentes ;<br /> par exemple l'accord parfait de ''do'' majeur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> degré de la gamme de ''do'' majeur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' mineur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''mi'' mineur ; * un accord parfait mineur peut appartenir à cinq gammes différentes ;<br />par exemple l'accord parfait de ''la'' mineur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> de la gamme de ''la'' mineur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''mi'' mineur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|III}}<sup>e</sup> degré de ''fa'' majeur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''do'' majeur ; * un accord de quinte diminuée peut appartenir à trois gammes différentes ;<br />par exemple, l'accord de quinte diminuée de ''si'' est l'accord construit sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' majeur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''la'' mineur et sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' mineur ; * un accord de quinte augmentée (à l'état fondamental) ne peut appartenir qu'à une seule gamme ;<br /> par exemple, l'accord de quinte augmentée de ''do'' est l'accord construit sur le {{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur. {| class="wikitable" |+ Notation jazz des triades |- | rowspan="2" colspan="2" | ! scope="col" colspan="2" | Tierce |- ! scope="col" | 3m ! scope="col" | 3M |- ! rowspan="3" | Quinte ! scope="row" | 5d | Xᵒ, X<sub>m</sub><sup>(♭5)</sup> || |- ! scope="row" | 5J | Xm, X– || X |- ! scope="row" | 5A | || X+, X<sup>(♯5)</sup> |} === Harmonisation par des accords de septième === [[Fichier:Harmonisation gamme do majeur par septiemes chiffre.svg|vignette|upright=2|Harmonisation de la gamme de do majeur par des accords de septième.]] Les accords de septième contiennent une dissonance et créent ainsi une tension. Ils sont très utilisés en jazz. Nous avons représenté ci-contre l'harmonisation de la gamme de ''do'' majeur. La constitution des accords est la suivantes : * tierce majeure (3M) ** quinte juste (5J) *** septième mineure (7m) : sur le degré V, c'est l'accord de septième de dominante V<sup>7</sup><sub>+</sub>, noté X<sup>7</sup> (X pour G), *** septième majeure (7M) : sur les degrés I et IV, appelés « accords de septième majeure » et notés aussi X<sup>maj7</sup> ou X<sup>Δ</sup> (X pour C ou F) ; * tierce mineure (3m) ** quinte juste (5J) *** septième mineure : sur les degrés ii, iii et vi, appelés « accords mineur septième » et notés Xm<sup>7</sup> ou X–<sup>7</sup> (X pour D, E ou A), ** quinte diminuée (5d) *** septième mineure (7m) : sur le degré vii, appelé « accord demi-diminué » (puisque seule la quinte est diminuée) et noté X<sup>∅</sup> ou Xm<sup>7(♭5)</sup> ou X–<sup>7(♭5)</sup> (X pour B) ;<br /> en musique classique, on considère que c'est un accord de neuvième de dominante sans fondamentale. Nous avons donc quatre types d'accords : X<sup>7</sup>, X<sup>maj7</sup>, Xm<sup>7</sup> et X<sup>∅</sup> En jazz, on ajoute souvent la quarte à l'accord de sous-dominante IV (sur le ''fa'' dans une gamme de ''do'' majeur) ; il s'agit ici d'une quarte augmentée (''fa''-''si'') et l'accord est surnommé « accord lydien » mais cette dénomination est erronée (il s'agit d'une mauvaise interprétation de textes antiques). C'est un accord de onzième sans neuvième (la onzième étant l'octave de la quarte), il est noté X<sup>maj7(♯11)</sup> ou X<sup>Δ(♯11)</sup> (ici, F<sup>maj7(♯11)</sup>, ''fa''-''la''-''do''-''mi''-''si'' ou ''fa''-''la''-''si''-''do''-''mi''). {| class="wikitable" |+ Chiffrage jazz des accords de septième |- ! scope="col" rowspan="2" | Tierce ! scope="col" rowspan="2" | Quinte ! scope="col" colspan="2" | Septième |- ! scope="col" | 7m ! scope="col" | 7M |- | rowspan="2" | 3m | 5d || X<sup>∅</sup>, X<sub>m</sub><sup>7(♭5)</sup>, X–<sup>7(♭5)</sup> || |- | rowspan="2" | 5J | X<sub>m</sub><sup>7</sup>, X–<sup>7</sup> || |- | rowspan="2" | 3M | X<sup>7</sup> || X<sup>maj7</sup>, X<sup>Δ</sup> |- | 5A || || X<sub>+</sub><sup>maj7</sup>, X<sub>+</sub><sup>Δ</sup> |} === Modulation et emprunt === Un morceau peut comporter des changements de tonalité; appelés « modulation ». Il y a parfois un court passage dans une tonalité différente, typiquement sur une ou deux mesures, avant de retourner dans la tonalité d'origine : on parle d'emprunt. Lorsqu'il y a une modulation ou un emprunt, les degrés changent. Un même accord peut donc avoir une fonction dans une partie du morceau et une autre fonction ailleurs. L'utilisation d'accord différents, et en particulier d'accord utilisant des altérations accidentelles, indique clairement une modulation. Nous avons vu précédemment que les modulations courantes sont : * les modulations dans les tons voisins ; * les modulations homonymes ; * les marches harmoniques. Une modulation entre une tonalité majeure et mineure change la couleur du passage, * la modulation la plus « douce » est entre les tonalités relatives (par exemple''do'' majeur et ''la'' mineur) car ces tonalités utilisent quasiment les mêmes notes ; * la modulation la plus « voyante » est la modulation homonyme (par exemple entre ''do'' majeur et ''do'' mineur). Une modulation commence souvent sur l'accord de dominante de la nouvelle tonalité. Pour analyser un œuvre, ou pour improviser sur une partie, il est important de reconnaître les modulations. La description de la successind es tonalités s'appelle le « parcours tonal ». === Exercices élémentaires === L'apprentissage des accords passe par quelques exercices élémentaires. '''1. Lire un accord''' Il s'agit de lecture de notes : des notes composant les accords sont écrites « empilées » sur une portée, il faut les lire en énonçant les notes de bas en haut. '''2. Reconnaître la « couleur » d'un accord''' On écoute une triade et il faut dire si c'est une triade majeure ou mineure. Puis, on complexifie l'exercice en ajoutant la septième. '''3. Chiffrage un accord''' Trouver le nom d'un accord à partir des notes qui le composent. '''4. Réalisation d'un accord''' Trouver les notes qui composent un accord à partir de son nom. '''5. Dictée d'accords''' On écoute une succession d'accords et il faut soit écrire les notes sur une portée, soit écrire les noms de accords. [[File:Exercice constitution accord basse chiffree.svg|thumb|Exercice : constitution d'accord à partir de la basse chiffrée.]] '''Exercices de basse chiffrée''' ''Réalisation d'un accord'' Sur la figure suivante, écrire les notes des accords correspondant à la basse chiffrée. Déterminer le degré de la fondamentale pour chaque accord en considérant que nous sommes dans la tonalité de ''sol'' majeur. {{boîte déroulante/début|titre=Solution}} [[File:Exercice constitution accord basse chiffree solution.svg|vignette|Solution.]] # La note de basse est un ''do''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''mi'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''sol''.<br />Le chiffrage « <sup>5</sup> » indique que c'est un accord dans son état fondamental (l'écart entre deux notes consécutives ne dépasse pas la tierce), la fondamentale est donc la basse, ''do'', qui est le degré IV de la tonalité. # La note de basse est un ''si''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''ré'', puis nous appliquons le chiffrage 6 et ajoutons la sixte, ''sol''.<br />Le chiffrage « <sup>6</sup> » indique que c'est un accord dans son premier renversement. En le remettant dans son état fondamental, nous obtenons ''sol-si-ré'', la fondamentale est donc la tonique, le degré I. # La note de basse est un ''la''. Nous ajoutons la tierce (chiffre 3), ''do'', et la sixte (6), ''fa''♯. Nous vérifions que le ''fa''♯ est la sensible (signe +)<br />Nous voyons un « blanc » entre les notes ''do'' et ''fa''♯. En descendant le ''fa''♯ à l'octave inférieure, nous obtenons un empilement de tierces ''fa''♯-''la-do'', le fondamentale est donc ''fa''♯, le degré VII. Nous pouvons le voir comme le deuxième renversement de l'accord de septième de dominante, sans fondamentale. # La note de basse est un ''fa''♯. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''la'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''do'' ; nous vérifions qu'il s'agit bien d'une quinte diminuée (le 5 est barré). Nous appliquons le chiffre 6 et ajoutons la sixte, ''ré''.<br />Nous voyons que les notes ''do'' et ''ré'' sont conjointes (intervalle de seconde). En descendant le ''ré'' à l'octave inférieure, nous obtenons un empilement de tierces ''ré-fa''♯-''la-do'', le fondamentale est donc ''ré'', le degré V. Nous constatons que l'accord chiffré est le premier renversement de l'accord de septième de dominante. {{boîte déroulante/fin}} {{clear}} ''Chiffrage d'accords'' [[Fichier:Exercice chiffrage accord basse chiffree.svg|vignette|Accords à chiffrer.]] Chiffrer les accords ci-contre. {{boîte déroulante/début|titre=Solution}} [[File:Exercice chiffrage accord basse chiffree solution.svg|vignette|Solution.]] # On relève les intervalles en partant de la basse : tierce majeure (3M) et quinte juste (5J). Le chiffrage complet est donc ''fa''<sup>5</sup><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''fa''<sup>5</sup>.<br /> On peut aussi reconnaître que c'est l'accord parfait sur la tonique de la tonalité de ''fa'' majeur dans son état fondamental, le chiffrage d'un accord parfait étant <sup>5</sup>. # On relève les intervalles en partant de la basse : quarte juste (4J), sixte majeure (6M). Le chiffrage complet est donc ''fa''<sup>6</sup><sub>4</sub>.<br /> On peut aussi reconnaître que c'est le second renversement de l'accord ''mi-sol-si'', sur la tonique de la tonalité de ''mi'' mineur, le chiffrage du second renversement d'un accord parfait étant <sup>6</sup><sub>4</sub>. # Les intervalles en partant de la basse sont : tierce mineure (3m), quinte diminuée (5d), sixte mineure (6m). Le chiffrage complet est donc ''mi''<sup>6</sup><small><s>5</s></small><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''mi''<sup>6</sup><sub><s>5</s></sub>.<br /> On reconnaît le premier renversement de l'accord ''do-mi-sol-si''♭, accord de septième de dominante de la tonalité de ''fa'' majeur. # Les intervalles en partant de la basse sont : tierce mineure (3m), quinte juste (5J), septième mineure (7m). Le chiffrage complet est donc ''ré''<sup>7</sup><small>5</small><sub>3</sub> ; c'est typique d'un accord de septième de dominante, son chiffrage est donc ''ré''<sup>7</sup><sub>+</sub>.<br /> On reconnaît l'accord de septième de dominante de la tonalité de ''sol'' mineur dans son état fondamental. {{boîte déroulante/fin}} {{clear}} [[File:Exercice constitution accord notation jazz.svg|thumb|Exercice : constitution d'un accord d'après son chiffrage en notation jazz.]] '''Exercices de notation jazz''' ''Réalisation d'un accord'' Sur la figure suivante, écrire les notes des accords correspondant aux chiffrages. {{boîte déroulante/début|titre=Solution}} [[File:Exercice constitution accord notation jazz solution.svg|thumb|Solution.]] # Il s'agit de la triade majeure de ''do'' dans son état fondamental. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''do-mi-sol''. # Il s'agit de la triade majeure de ''sol''. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''sol-si-ré''. On renverse l'accord afin que la basse soit le ''si'', l'accord est donc ''si-ré-sol''. # Il s'agit de l'accord demi-diminué de ''fa''♯. Les intervalles sont la tierce mineure (3m), la quinte diminuée (5d) et la septième mineure (7m). Les notes sont donc ''fa''♯-''la-do-mi''. Nous renversons l'accord afin que la basse soit le ''la'', l'accord est donc ''a-do-mi-fa''♯. # Il s'agit de l'accord de septième de ''ré''. Les intervalles sont donc la tierce majeure (3M), la quinte juste (5J) et la septième mineure (7m). Les notes sont ''ré-fa''♯''-la-do''. Nous renversons l'accord afin que la basse soit le ''fa''♯, l'accord est donc ''fa''♯''-la-do-ré''. {{boîte déroulante/fin}} {{clear}} ''Chiffrage d'accords'' [[File:Exercice chiffrage accord notation jazz.svg|thumb|Accords à chiffrer.]] Chiffrer les accords ci-contre. {{boîte déroulante/début|titre=Solution}} [[File:Exercice chiffrage accord notation jazz solution.svg|thumb|Solution.]] # Les notes sont toutes sur des interlignes consécutifs, c'est donc un empilement de tierces ; l'accord est dans son état fondamental. Les intervalles sont une tierce majeure (''fa-la'' : 3M) et une quinte juste (''fa-do'' : 5J), c'est donc la triade majeure de ''fa''. Le chiffrage est F. # Il y a un blanc dans l'empilement des notes, c'est donc un accord renversé. En permutant les notes pour n'avoir que des tierces, on trouve l'accord ''mi-sol-si''. Les intervalles sont une tierce mineure (''mi-sol'' : 3m) et une quinte juste (''mi-si'' : 5J), c'est donc la triade mineure de ''mi'' avec un ''si'' à la basse. Le chiffrage est Em/B ou E–/B. # Il y deux notes conjointes, c'est donc un renversement. L'état fondamental de cet accord est ''do-mi-sol-si''♭. Les intervalles sont une tierce majeure (''do-mi'' : 3M), une quinte juste (''do-sol'' : 5J) et une septième mineure (''do-si''♭ : 7m). C'est donc l'accord de ''do'' septième avec un ''mi'' à la basse, chiffré C<sup>7</sup>/E. # Les notes sont toutes sur des interlignes consécutifs, l'accord est dans son état fondamental. Les intervalles sont la tierce mineure (''ré-fa'' : 3m), une quinte juste (''ré-la'' : 5J) et une septième mineure (''ré-do'' : 7m). C'est donc l'accord de ''ré'' mineur septième, chiffré Dm<sup>7</sup> ou D–<sup>7</sup>. {{boîte déroulante/fin}} {{clear}} == Harmonie fonctionnelle == Le choix des accords et de leur succession — la progression des accords — est un élément important d'un morceau, de sa composition. Le compositeur ou la compositrice a bien sûr une liberté totale, mais pour faire des choix, il faut comprendre les conséquences de ces choix, et donc ici, les effets produits par les accords et leur progression. Une des manières d'aborder le sujet est l'harmonie fonctionnelle. === Les trois fonctions des accords === En harmonie tonale, on considère que les accords ont une fonction. Il existe trois fonctions : * la fonction de tonique, {{Times New Roman|I}} ; * la fonction de sous-dominante, {{Times New Roman|IV}} ; * la fonction de dominante, {{Times New Roman|V}}. L'accord de tonique, {{Times New Roman|I}}, est l'accord « stable » de la tonalité par excellence. Il conclut en général les morceaux, et ouvre souvent les morceaux ; il revient fréquemment au cours du morceau. L'accord de dominante, {{Times New Roman|V}}, est un accord qui introduit une instabilité, une tension. En particulier, il contient la sensible (degré {{Times New Roman|VI}}), qui est une note « aspirée » vers la tonique. Cette tension, qui peut être renforcée par l'utilisation d'un accord de septième, est fréquemment résolue par un passage vers l'accord de tonique. Nous avons donc deux mouvements typiques : {{Times New Roman|I}} → {{Times New Roman|V}} (création d'une tension, d'une attente) et {{Times New Roman|V}} → {{Times New Roman|I}} (résolution d'une tension). Les accords de tonique et de dominante ont le cinquième degré en commun, cette note sert donc de pivot entre les deux accords. L'accord de sous-dominante, {{Times New Roman|IV}}, est un accord qui introduit lui aussi une tension, mais moins grande : il ne contient pas la sensible. Notons que s'il est une quarte au-dessus de la tonique, il est aussi une quinte en dessous d'elle ; il est symétrique de l'accord de dominante. Il a donc un rôle similaire à l'accord de dominante, mais atténué. L'accord de sous-dominante aspire soit vers l'accord de dominante, très proche, et l'on a alors une augmentation de la tension ; soit vers l'accord de tonique, un retour vers la stabilité (il a alors un rôle semblable à la dominante). Du fait de ces deux bifurcations possibles — augmentation de la tension ({{Times New Roman|IV}} → {{Times New Roman|V}}) ou retour à la stabilité ({{Times New Roman|IV}} → {{Times New Roman|I}}) —, l'utilisation de l'accord de sous-dominante introduit un certain flottement : si l'on peut facilement prédire l'accord qui suit un accord de dominante, on ne peut pas prédire ce qui suit un accord de sous-dominante. Notons que la composition ne consiste pas à suivre ces règles de manière stricte, ce qui conduirait à des morceaux stéréotypés et plats. Le plaisir d'écoute joue sur une alternance entre satisfaction d'une attente (respect des règles) et surprise (rompre les règles). === Accords remplissant ces fonctions === Les accords sur les autres degrés peuvent se ramener à une de ces trois fonctions : * {{Times New Roman|II}} : fonction de sous-dominante {{Times New Roman|IV}} ; * {{Times New Roman|III}} (très peu utilisé en mode mineur en raison de sa dissonance) et {{Times New Roman|VI}} : fonction de tonique {{Times New Roman|I}} ; * {{Times New Roman|VII}} : fonction de dominante {{Times New Roman|V}}. En effet, les accords étant des empilements de tierces, des accords situés à une tierce l'un de l'autre — {{Times New Roman|I}} ↔ {{Times New Roman|III}}, {{Times New Roman|II}} ↔ {{Times New Roman|IV}}, {{Times New Roman|V}} ↔ {{Times New Roman|VII}}, {{Times New Roman|VI}} ↔ {{Times New Roman|VIII}} ( = {{Times New Roman|I}}) — ont deux notes en commun. On retrouve le fait que l'accord sur le degré {{Times New Roman|VII}} est considéré comme un accord de dominante sans tonique. En mode mineur, l'accord sur le degré {{Times New Roman|III}} est évité, il n'a donc pas de fonction. {|class="wikitable" |+ Fonction des accords |- ! scope="col" | Fondamentale ! scope="col" | Fonction |- | {{Times New Roman|I}} || tonique |- | {{Times New Roman|II}} || sous-dominante faible |- | {{Times New Roman|III}} || tonique faible |- | {{Times New Roman|IV}} || sous-dominante |- | {{Times New Roman|V}} || dominante |- | {{Times New Roman|VI}} || tonique faible |- | {{Times New Roman|VII}} || dominante faible |} Par exemple en ''do'' majeur : * fonction de tonique : '''''do''<sup>5</sup> (C)''', ''mi''<sup>5</sup> (E–), ''la''<sup>5</sup> (A–) ; * fonction de sous-dominante : '''''fa''<sup>5</sup> (F)''', ''ré''<sup>5</sup> (D–) ; * fonction de dominante : '''''sol''<sup>5</sup> (G)''' ou ''sol''<sup>7</sup><sub>+</sub> (G<sup>7</sup>), ''si''<sup> <s>5</s></sup> (B<sup>o</sup>). En ''la'' mineur harmonique : * fonction de tonique : '''''la''<sup>5</sup> (A–)''', ''fa''<sup>5</sup> (F) [, rarement : ''do''<sup>+5</sup> (C<sup>+</sup>)] ; * fonction de sous-dominante : '''''ré''<sup>5</sup> (D–)''', ''si''<sup> <s>5</s></sup> (B<sup>o</sup>) ; * fonction de dominante : '''''mi''<sup>5</sup> (E)''' ou ''mi''<sup>7</sup><sub>+</sub> (E<sup>7</sup>), ''sol''♯<sup> <s>5</s></sup> (G♯<sup>o</sup>). Le fait d'utiliser des accords différents pour remplir une fonction permet d'enrichir l'harmonie, et de jouer sur l'équilibre entre satisfaction d'une attente (on respecte les règles sur les fonctions) et surprise (mais on n'utilise pas l'accord attendu). === Les dominantes secondaires === On utilise aussi des accords de septième dominante se fondant sur un autre degré que la dominante de la gamme ; on parle de « dominante secondaire ». Typiquement, avant un accord de septième de dominante, on utilise parfois un accord de dominante de dominante, dont le degré est alors noté « {{Times New Roman|V}} de {{Times New Roman|V}} » ou « {{Times New Roman|V}}/{{Times New Roman|V}} » ; la fondamentale est de l'accord est alors situé cinq degrés au-dessus de la dominante ({{Times New Roman|V}}), c'est donc le degré {{Times New Roman|IX}}, c'est-à-dire le degré {{Times New Roman|II}} de la tonalité en cours). Ou encore, on utilise un accord de dominante du degré {{Times New Roman|IV}} (« {{Times New Roman|V}} de {{Times New Roman|IV}} », la fondamentale est alors le degré {{Times New Roman|I}}) avant un accord sur le degré {{Times New Roman|IV}} lui-même. Par exemple, en tonalité de ''do'' majeur, on peut trouver un accord ''ré - fa''♯'' - la - do'' (chiffré {{Times New Roman|V}} de {{Times New Roman|V}}<sup>7</sup><sub>+</sub>), avant un accord ''sol - si - ré - fa'' ({{Times New Roman|V}}<sup>7</sup><sub>+</sub>). L'accord ''ré - fa''♯'' - la - do'' est l'accord de septième de dominante des tonalités de ''sol''. Dans la même tonalité, on pourra utiliser un accord ''do - mi - sol - si''♭ ({{Times New Roman|V}} de {{Times New Roman|IV}}<sup>7</sup><sub>+</sub>) avant un accord ''fa - la - do'' ({{Times New Roman|IV}}<sup>5</sup>). Le recours à une dominante secondaire peut atténuer une transition, par exemple avec un enchaînement ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> → ''fa''<sup>5</sup> (C → C<sup>7</sup> → F) qui correspond à un enchaînement {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}} : le passage ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> (C → C<sup>7</sup>) se fait en ajoutant une note (le ''si''♭) et rend naturel le passage ''do'' → ''fa''. Sur les sept degré de la gamme, on ne considère en général que cinq dominantes secondaires : en effet, la dominante du degré {{Times New Roman|I}} est la dominante « naturelle, primaire » de la tonalité (et n'est donc pas secondaire) ; et utiliser la dominante de {{Times New Roman|VII}} consisterait à considérer l'accord de {{Times New Roman|VII}} comme un accord propre, on évite donc les « {{Times New Roman|V}} de “{{Times New Roman|V}}” » (mais les « “{{Times New Roman|V}}” de {{Times New Roman|V}} » sont tout à fait « acceptables »). === Enchaînements classiques === Nous avons donc vu que l'on trouve fréquemment les enchaînements suivants : * pour créer une instabilité : ** {{Times New Roman|I}} → {{Times New Roman|V}}, ** {{Times New Roman|I}} → {{Times New Roman|IV}} (instabilité moins forte mais incertitude sur le sens d'évolution) ; * pour maintenir l'instabilité : ** {{Times New Roman|IV}} → {{Times New Roman|V}} ; * pour résoudre l'instabilité : ** {{Times New Roman|IV}} → {{Times New Roman|I}}, ** {{Times New Roman|V}} → {{Times New Roman|I}}, cas particuliers (voir plus bas) : *** {{Times New Roman|V}}<sup>+4</sup> → {{Times New Roman|I}}<sup>6</sup>, *** {{Times New Roman|I}}<sup>6</sup><sub>4</sub> → {{Times New Roman|V}}<sup>7</sup><sub>+</sub> → {{Times New Roman|I}}<sup>5</sup>. Les degrés indiqués ci-dessus sont les fonctions ; on peut donc utiliser les substitutions suivantes : * {{Times New Roman|I}} par {{Times New Roman|VI}} et, en tonalité majeure, {{Times New Roman|III}} ; * {{Times New Roman|IV}} par {{Times New Roman|II}} ; * {{Times New Roman|V}} par {{Times New Roman|VII}}. Pour enrichir l'harmonie, on peut utiliser les dominantes secondaires, en particulier : * {{Times New Roman|V}} de {{Times New Roman|V}} ({{Times New Roman|II}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|V}}, * {{Times New Roman|V}} de {{Times New Roman|IV}} ({{Times New Roman|I}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|IV}}. On peut enchaîner les enchaînements, par exemple {{Times New Roman|I}} → {{Times New Roman|IV}} → {{Times New Roman|V}}, ou encore {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}}… En jazz, on utilise très fréquemment l'enchaînement {{Times New Roman|II}} → {{Times New Roman|V}} → {{Times New Roman|I}} (deux-cinq-un). On peut bien sûr avoir d'autres enchaînements, mais ces règles permettent d'analyser un grand nombre de morceaux, et donnent des clefs utiles pour la composition. Nous voyons ci-après un certain nombre d'enchaînements courants dans différents styles == Exercice == Un hautboïste travaille la sonate en ''do'' mineur S. 277 de Heinichen. Sur le deuxième mouvement ''Allegro'', il a du mal à travailler un passage en raison des altérations accidentelles. Sur la suggestion de sa professeure, il décide d'analyser la progression d'accords sous-jacente afin que les altérations deviennent logiques. Il s'agit d'un duo hautbois et basson pour lequel les accords ne sont pas chiffrés, le basson étant ici un instrument soliste et non pas un élément de la basse continue. Sur l'extrait suivant, déterminez les basses et la qualité (chiffrage) des accords sous-jacents. Commentez. [[Fichier:Sonate hautbois basson heinichen 2e mvt mes49.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen.]] {{note|L'œuvre est en ''do'' mineur et devrait donc avoir trois bémols à la clef, or ici il n'y en a que deux. En effet, le ''la'' pouvant être bécarre en mode mineur mélodique ascendant, le compositeur a préféré le noter explicitement en altération accidentelle lorsque l'on est en mode mélodique naturel, harmonique ou mélodique descendant. C'est un procédé assez courant à l'époque baroque.}} {{boîte déroulante/début|titre=Solution}} Une des difficultés ici est que les arpèges joués par les instruments sont agrémentés de notes de passage. Les notes de la basse (du basson) sont différentes entre le premier et le deuxième temps de chaque mesure et ne peuvent pas appartenir au même accord. On a donc un accord par temps. Sur le premier temps de chaque mesure, le basson joue une octave. La note concernée est donc la basse de chaque accord. Pour savoir s'il s'agit d'un accord à l'état fondamental ou d'un renversement, on regarde ce que joue le hautbois : dans un mouvement conjoint (succession d'intervalles de secondes), il est difficile de distinguer les notes de l'arpège des notes de passage, mais : les notes des grands intervalles font partie de l'accord. Ainsi, sur le premier temps de la première mesure (la basse est un ''mi''♭), on a une sixte descendante ''sol''-''si''♭ et, à la fin du temps, une tierce descendante ''sol''-''mi''♭. L'accord est donc ''mi''♭-''sol''-''si''♭, c'est un accord de quinte (accord parfait à l'état fondamental). À la fin du premier temps, le basson joue un ''do'', c'est donc une note étrangère. Sur le second temps de la première mesure, le basson joue une tierce ascendante ''fa''-''la''♭, la première note est la basse de l'accord et la seconde une des notes de l'accord. Le hautbois commence par une sixte descendante ''la''♭-''do'', l'accord est donc ''fa''-''la''♭-''do'', un accord de quinte (accord parfait à l'état fondamental). Le ''do'' du basson la fin du premier temps est donc une anticipation. Les autres notes étrangères de la première mesure sont des notes de passage. Mais il faut faire attention : en suivant ce principe, sur les premiers temps des deuxième et troisième mesure, nous aurions des accords de septième d'espèce (puisque la septième est majeure). Or, on ne trouve pas, ou alors exceptionnellement, d'accord de septième d'espèce dans le baroque, mais quasi exclusivement des accords de septième de dominante. Donc au début de la deuxième mesure, le ''la''♮ est une appoggiature du ''si''♭, l'accord est donc ''si''♭-''ré''-''fa'', un asscord de quinte. De même, au début de la troisième mesure, le ''sol'' est une appoggiature du ''la''♭. Il faut donc se méfier d'une analyse purement « mathématique ». Il faut s'attacher à ressentir la musique, et à connaître les styles, pour faire une analyse pertinente. Ci-dessous, nous avons grisé les notes étrangères. [[Fichier:Sonate hautbois basson heinichen 2e mvt mes49 analyse.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen. Analyse de la progression harmonique.]] Le chiffrage jazz équivalent est : : | E♭ F– | B♭<sup>Δ</sup> E♭ | A♭<sup>Δ</sup> D– | G … Nous remarquons une progression assez régulière : : ''mi''♭ ↗[2<sup>de</sup>] ''fa'' | ↘[5<sup>te</sup>] ''si''♭ ↗[4<sup>te</sup>] ''mi''♭ | ↘[5<sup>te</sup>] ''la''♭ ↗[4<sup>te</sup>] ''ré'' | ↘[5<sup>te</sup>] ''sol'' Le ''mi''♭ est le degré {{Times New Roman|III}} de la tonalité principale (''do'' mineur), c'est donc une tonique faible ; il « joue le même rôle » qu'un ''do''. S'il y avait eu un accord de ''do'' au début de l'extrait, on aurait eu une progression parfaitement régulière ↗[4<sup>te</sup>] ↘[5<sup>te</sup>]. Nous avons les modulations suivantes : * mesure 49 : ''do'' mineur naturel (le ''si''♭ n'est pas une sensible) avec un accord sur “{{Times New Roman|I}}” (tonique faible, {{Times New Roman|III}}, pour la première analyse, ou bien tonique forte, {{Times New Roman|I}}, pour la seconde) suivi d'un accord sur {{Times New Roman|IV}} ; * mesure 50 : ''si''♭ majeur avec un accord sur {{Times New Roman|I}} suivi d'un accord sur {{Times New Roman|IV}} ; * mesure 51 : ''la''♭ majeur avec un accord sur {{Times New Roman|I}}, et emprunt à ''do'' majeur avec un accord sur {{Times New Roman|II}} ({{Times New Roman|IV}} faible). On a donc une marche harmonique {{Times New Roman|I}} → {{Times New Roman|IV}} qui descend d'une seconde majeure (un ton) à chaque mesure (''do'' → ''si''♭ → ''la''♭), avec une exception sur la dernière mesure (modulation en cours de mesure et descente d'une seconde mineure au lieu de majeure). Ce passage est donc construit sur une régularité, une règle qui crée un effet d'attente — enchaînement {{Times New Roman|I}}<sup>5</sup> → {{Times New Roman|IV}}<sup>5</sup> avec une marche harmonique d'une seconde majeure descendante —, et des « surprises », des exceptions au début — ce n'est pas un accord {{Times New Roman|I}}<sup>5</sup> mais un accord {{Times New Roman|III}}<sup>5</sup> — et à la fin — modulation en milieu de mesure et dernière descente d'une seconde mineure (½t ''la''♭ → ''sol''). L'extrait ne permet pas de le deviner, mais la mesure 52 est un retour en ''do'' mineur, avec donc une modulation sur la dominante (accord de ''sol''<sup>7</sup><sub>+</sub>, G<sup>7</sup>). {{boîte déroulante/fin}} == Progression d'accords == Comme pour la mélodie, la succession des accords dans un morceau, la progression d'accords, suit des règles. Et comme pour la mélodie, les règles diffèrent d'un style musical à l'autre et la créativité consiste à parfois ne pas suivre ces règles. Et comme pour la mélodie, on part d'un ensemble de notes organisé, d'une gamme caractéristique d'une tonalité, d'un mode. Les accords les plus utilisés pour une tonalité donnée sont les accords dont la fondamentale sont les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la tonalité, en particulier la triade {{Times New Roman|I}}, appelée « accord parfait » ou « accord de tonique », et l'accord de septième {{Times New Roman|V}}, appelé « septième de dominante ». Le fait d'avoir une progression d'accords qui se répète permet de structurer un morceau. Pour les morceaux courts, il participe au plaisir de l'écoute et facilite la mémorisation (par exemple le découpage couplet-refrain d'une chanson). Sur les morceaux longs, une trop grande régularité peut introduire de la lassitude, les longs morceaux sont souvent découpés en parties présentant chacune une progression régulière. Le fait d'avoir une progression régulière permet la pratique de l'improvisation : cadence en musique classique, solo en jazz et blues. ; Note : Le terme « cadence » désigne plusieurs choses différentes, et notamment en harmonie : :* une partie improvisée dans un opéra ou un concerto, sens utilisé ci-dessus ; :* une progression d'accords pour ponctuer un morceau et en particulier pour le conclure, sens utilisé dans la section suivante. === Accords peu utilisés === En mode mineur, l'accord de quinte augmentée {{Times New Roman|III<sup>+5</sup>}} est très peu utilisé. C'est un accord dissonant ; il intervient en général comme appogiature de l'accord de tonique (par exemple en ''la'' mineur : {{Times New Roman|III<sup>+5</sup>}} ''do'' - ''mi'' - ''sol''♯ → {{Times New Roman|I<sup>6</sup>}} ''do'' - ''mi'' - ''la''), ou de l'accord de dominante ({{Times New Roman|III<sup>6</sup><sub>+3</sub>}} ''mi'' - ''sol''♯ - ''do'' → {{Times New Roman|V<sup>5</sup>}} ''mi'' - ''sol''♯ - ''si''). Il peut être aussi utilisé comme préparation à l'accord de sous-dominante (enchaînement {{Times New Roman|III}} → {{Times New Roman|IV}}). Par ailleurs, il a une constitution symétrique — c'est l'empilement de deux tierces majeures — et ses renversements ont les mêmes intervalles à l'enharmonie près (quinte augmentée/sixte mineure, tierce majeure/quarte diminuée). De ce fait, un même accord est commun, par renversement et à l'enharmonie près, à trois tonalités : le premier renversement de l'accord ''do'' - ''mi'' - ''sol''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur) est enharmonique à ''mi'' - ''sol''♯ - ''si''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''do''♯ mineur) ; le second renversement est enharmonique à ''la''♭ - ''do'' - ''mi'' ({{Times New Roman|III}}<sup>e</sup> degré de ''fa'' mineur). === Accords très utilisés === Les trois accords les plus utilisés sont les accords de tonique (degré {{Times New Roman|I}}), de sous-dominante ({{Times New Roman|IV}}) et de dominante ({{Times New Roman|V}}). Ils interviennent en particulier en fin de phrase, dans les cadences. L'accord de dominante sert souvent à introduire une modulation : la modulation commence sur l'accord de dominante de la nouvelle tonalité. On note que l'accord de sous-dominante est situé une quinte juste en dessous de la tonique, les accords de dominante et de sous-dominante sont donc symétriques. En jazz, on utilise également très fréquemment l'accord de la sus-tonique (degré {{Times New Roman|II}}), souvent dans des progressions {{Times New Roman|II}} - {{Times New Roman|V}} (- {{Times New Roman|I}}). Rappelons que l'accord de sus-tonique a la fonction de sous-dominante. === Cadences et ''turnaround'' === Le terme « cadence » provient de l'italien ''cadenza'' et désigne la « chute », la fin d'un morceau ou d'une phrase musicale. On distingue deux types de cadences : * les cadences conclusive, qui créent une sensation de complétude ; * les cadences suspensives, qui crèent une sensation d'attente. ==== Cadence parfaite ==== [[Fichier:Au clair de le lune cadence parfaite.midi|thumb|''Au clair de la lune'', harmonisé avec une cadence parfaite (italienne).]] [[Fichier:Au clair de le lune mineur cadence parfaite.midi|thumb|''Idem'' mais en mode mineur harmonique.]] La cadence parfaite est l'enchaînement de l'accord de dominante suivi de l'accord parfait : {{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}}, les deux accord étant à l'état fondamental. Elle donne une impression de stabilité et est donc très souvent utilisée pour conclure un morceau. C'est une cadence conclusive. On peut aussi utiliser l'accord de septième de dominante, la dissonance introduisant une tension résolue par l'accord parfait : {{Times New Roman|V<sup>7</sup><sub>+</sub> - I<sup>5</sup>}}. Elle est souvent précédée de l'accord construit sur le IV<sup>e</sup> degré, appelé « accord de préparation », pour former la cadence italienne : {{Times New Roman|IV<sup>5</sup> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}}. Elle est également souvent précédée du second renversement de l'accord de tonique, qui est alors appelé « appoggiature de la cadence » : {{Times New Roman|I<sup>6</sup><sub>4</sub> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}} (on remarque que les accords {{Times New Roman|I}}<sup>6</sup><sub>4</sub> et {{Times New Roman|V}}<sup>5</sup> ont la basse en commun, et que l'on peut passer de l'un à l'autre par un mouvement conjoint sur les autres notes). {{clear}} ==== Demi-cadence ==== [[Fichier:Au clair de le lune demi cadence.midi|thumb|''Au clair de la lune'', harmonisé avec une demi-cadence.]] Une demi-cadence est une phrase ou un morceau se concluant sur l'accord construit sur le cinquième degré. Il provoque une sensation d'attente, de suspens. Il s'agit en général d'une succession {{Times New Roman|II - V}} ou {{Times New Roman|IV - V}}. C'est une cadence suspensive. On utilise rarement un accord de septième de dominante. {{clear}} ==== Cadence rompue ou évitée ==== La cadence rompue, ou cadence évitée, est succession d'un accord de dominante et d'un accord de sus-dominante, {{Times New Roman|V}} - {{Times New Roman|VI}}. C'est une cadence suspensive. ==== Cadence imparfaite ==== Une cadence imparfaite est une cadence {{Times New Roman|V - I}}, comme la cadence parfaite, mais dont au moins un des deux accords est dans un état renversé. ==== Cadence plagale ==== La cadence plagale — du grec ''plagios'', oblique, en biais — est la succession de l'accord construit sur le quatrième degré, suivi de l'accord parfait : {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}. Elle peut être utilisée après une cadence parfaite ({{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}} - {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}). Elle donne un caractère solennel, voire religieux — elle est parfois appelée « cadence amen » —, elle a un côté antique qui rappelle la musique modale et médiévale<ref>{{lien web |url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/la-cadence-amen-ou-comment-se-dire-adieu-7191921 |titre=La cadence « Amen » ou comment se dire adieu |auteur=Max Dozolme (MAXXI Classique) |site=France Musique |date=2025-04-25 |consulté le=2025-04-25}}.</ref>. C'est une cadence conclusive. ==== {{lang|en|Turnaround}} ==== [[Fichier:Au clair de le lune turnaround.midi|thumb|Au clair de la lune, harmonisé en style jazz : accords de 7{{e}}, anatole suivie d'un ''{{lang|en|turnaround}}'' ii-V-I.]] Le terme ''{{lang|en|turnaround}}'' signifie revirement, retournement. C'est une succession d'accords que fait la transition entre deux parties, en créant une tension-résolution. Le ''{{lang|en|turnaround}}'' le plus courant est la succession {{Times New Roman|II - V - I}}. On utilise également fréquemment l'anatole : {{Times New Roman|I - VI - II - V}}. {| class="wikitable" |+ Progressions typiques d'accords dans une tonalité majeure ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Cadence<br />parfaite<br /> {{Times New Roman|V - I}} ! scope="col" | Cadence<br />italienne<br />{{Times New Roman|IV - V - I}} ! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou IV - V}} ! scope="col" | Cadence<br />plagale<br />{{Times New Roman|IV - I}} ! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}} ! scope="col" | Anatole<br />{{Times New Roman|I - vi - ii - V}} |- |''Do'' majeur || || G - C || F - G - C || Dm - G ou F - G || F - C || Dm - G - C || C - Am - Dm - G |- |''Sol'' majeur || ''fa''♯ || D - G || C - D - G || Am - D ou C - D || C - G || Am - D - G || G - Em - Am - D |- |''Ré'' majeur || ''fa''♯, ''do''♯ || A - D || G - A - D || Em - A ou G - A || G - D || Em - A - D || D - Bm - Em - A |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || E - A || D - E - A || Bm - E ou D - E || D - A || Bm - E - A || A - F♯m - B - E |- | ''Fa'' majeur || ''si''♭ || C - F || B♭ - C - F || Gm - C ou B♭ - C || B♭ - F || Gm - C - F || F - Dm - Gm - C |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || F - B♭ || E♭ - F - B♭ || Cm - F ou E♭ - F || E♭ - B♭ || Cm - F - B♭ || B♭ - Gm - Cm - F |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || B♭ - E♭ || A♭ - B♭ - E♭ || Fm - B♭ ou A♭ - B♭ || A♭ - E♭ || Fm - B♭ - E♭ || Gm - Cm - Fm - B♭ |} {| class="wikitable" |+ Progressions typiques d'accords dans une tonalité mineure ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Cadence<br />parfaite<br />{{Times New Roman|V - i}} ! scope="col" | Cadence<br />italienne<br />{{Times New Roman|iv - V - i}} ! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou iv - V}} ! scope="col" | Cadence<br />plagale<br />{{Times New Roman|iv - i}} ! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}} ! scope="col" | Anatole<br />{{Times New Roman|i - VI - ii - V}} |- | ''La'' mineur<br />harmonique || || E - Am || Dm - E - Am || B° - E ou Dm - E || Dm - Am || B° - E - Am || Am - F - B° - E |- | ''Mi'' mineur<br />harmonique || ''fa''♯ || B - Em || Am - B - Em || F♯° - B ou Am - B || Am - Em || F♯° - B - Em || Em - C - F♯° - B |- | ''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || F♯ - Bm || Em - F♯ - Bm || C♯° - F♯ ou Em - F♯ || Em - Bm || C♯° - F♯ - Bm || Bm - G - C♯° - F♯ |- | ''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || C♯ - F♯m || Bm - C♯ - F♯m || G♯° - C♯ ou Bm - C♯ || Bm - F♯m || G♯° - C♯ - F♯m || A+ - D - G♯° - C♯ |- | ''Ré'' mineur<br />harmonique || ''si''♭ || A - Dm || Gm - A - Dm || E° - A ou Gm - A || Gm - Dm || E° - A - Dm || Dm - B♭ - E° - A |- | ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || D - Gm || Cm - D - Gm || A° - D ou Cm - D || Cm - Gm|| A° - D - Gm || Gm - E♭ - A° - D |- | ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || G - Cm || Fm - G - Cm || D° - G ou Fm - G || Fm - Dm || D° - G - Cm || Cm - A♭ - D° - G |} ==== Exemple : ''La Mer'' ==== : {{lien web | url = https://www.youtube.com/watch?v=PXQh9jTwwoA | titre = Charles Trenet - La mer (Officiel) [Live Version] | site = YouTube | auteur = Charles Trenet | consulté le = 2020-12-24 }} Le début de ''La Mer'' (Charles Trenet, 1946) est en ''do'' majeur et est harmonisé par l'anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (C - Am - Dm - G<sup>7</sup>) sur deux mesures, jouée deux fois ({{Times New Roman|1=<nowiki>|I-vi|ii-V</nowiki><sup>7</sup><nowiki>|</nowiki>}} × 2). Viennent des variations avec les progressions {{Times New Roman|I-III-vi-V<sup>7</sup>}} (C - E - Am - G<sup>7</sup>) puis la « progression ’50s » (voir plus bas) {{Times New Roman|I-vi-IV-VI<sup>7</sup>}} (C - Am - F - A<sup>7</sup>, on remarque que {{Times New Roman|IV}}/F est le relatif majeur du {{Times New Roman|ii}}/Dm de l'anatole), jouées chacune une fois sur deux mesure ; puis cette première partie se conclut par une demie cadence {{Times New Roman|ii-V<sup>7</sup>}} sur une mesure puis une dernière anatole sur trois mesures ({{Times New Roman|1=<nowiki>|I-vi|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}}). Cela constitue une première partie « A » sur douze mesures qui se termine par une demi-cadence ({{Times New Roman|ii-V<sup>7</sup>}}) qui appelle une suite. Cette partie A est jouée une deuxième fois mais la fin est modifiée pour la transition : les deux dernières mesures {{Times New Roman|<nowiki>|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}} deviennent {{Times New Roman|<nowiki>|ii-V</nowiki><sup>7</sup><nowiki>|I|</nowiki>}} (|Dm-G7|C|), cette partie « A’ » se conclut donc par une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}). Le morceau passe ensuite en tonalité de ''mi'' majeur, donc une tierce au dessus de ''do'' majeur, sur six mesures. Cette partie utilise une progression ’50s {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (E - C♯m - A - B<sup>7</sup>), qui est rappelons-le une variation de l'anatole, l'accord {{Times New Roman|ii}} (Fm) étant remplacé par son relatif majeur {{Times New Roman|IV}} (A). Cette anatole modifiée est jouée deux fois puis la partie en ''mi'' majeur se conclut par l'accord parfait {{Times New Roman|I}} joué sur deux mesures (|E|E|), on a donc, avec la mesure précédente, avec une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}). Suivent ensuite six mesures en ''sol'' majeur, donc à nouveau une tierce au dessus de ''mi'' majeur. Elle comporte une progression {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (G - Em - C - D<sup>7</sup>), donc anatole avec substitution du {{Times New Roman|ii}}/Am par son relatif majeur {{Times New Roman|VI}}/C (progression ’50s), puis une anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (G - Em - Am - D<sup>7</sup>) et deux mesure sur la tonique {{Times New Roman|I-I<sup>7</sup>}} (G - G<sup>7</sup>), formant à nouveau une cadence parfaite. La fin sur un accord de septième, dissonant, appelle une suite. Cette partie « B » de douze mesures comporte donc deux parties similaires « B1 » et « B2 » qui forment une marche harmonique (montée d'une tierce). Le morceau se conclut par une reprise de la partie « A’ » et se termine donc par une cadence parfaite. Nous avons une structure A-A’-B-A’ sur 48 mesures, proche la forme AABA étudiée plus loin. Donc ''La Mer'' est un morceau structuré autour de l'anatole avec des variations (progression ’50s, substitution du {{Times New Roman|ii}} par son relatif majeur {{Times New Roman|IV}}) et comportant une marche harmonique dans sa troisième partie. Les parties se concluent par des ''{{lang|en|turnarounds}}'' sous la forme d'une cadence parfaite ou, pour la partie A, par une demi-cadence. {| border="1" rules="rows" frame="hsides" |+ Structure de ''La Mer'' |- align="center" | | colspan="12" | ''do'' majeur | |- align="center" ! scope="row" rowspan=2 | A | colspan="2" | anatole | colspan="2" | // | colspan="2" | variation | colspan="2" | ’50s | ½ c. | colspan="3" | anatole | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii}} || <nowiki>|</nowiki> {{Times New Roman|V<sup>7</sup>}} || <nowiki>|</nowiki> |- align="center" ! scope="row" rowspan="2" | A’ | colspan="2" | anatole | colspan="2" | // | colspan="2" | variation | colspan="2" | ’50s | ½ c. | colspan="2" | anatole | c.p. | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> |- align="center" | | colspan="6" | B1 : ''mi'' majeur | colspan="6" background="lightgray" | B2 : ''sol'' majeur | |- align="center" ! scope="row" rowspan="2" | B | colspan="2" | ’50s | colspan="2" | // |colspan="2" | c.p. | colspan="2" | ’50s | colspan="2" | // |colspan="2" | c.p. | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I<sup>7</sup>}} || <nowiki>|</nowiki> |- ! scope="row" | A’ | colspan="12" | | |} === Progression blues === La musique blues est apparue dans les années 1860. Elle est en général bâtie sur une grille d'accords ''({{lang|en|changes}})'' immuable de douze mesures ''({{lang|en|twelve-bar blues}})''. C'est sur cet accompagnement qui se répète que s'ajoute la mélodie — chant et solo. Cette structure est typique du blues et se retrouve dans ses dérivés comme le rock 'n' roll. Le rythme est toujours un rythme ternaire syncopé ''({{lang|en|shuffle, swing, groove}}, ''notes inégales'')'' : la mesure est à quatre temps, mais la noire est divisée en noire-croche en triolet, ou encore triolet de croche en appuyant la première et la troisième. La mélodie se construit en général sur une gamme blues de six degrés (gamme pentatonique mineure avec une quarte augmentée), mais bien que la gamme soit mineure, l'harmonie est construite sur la gamme majeure homonyme : un blues en ''fa'' a une mélodie sur la gamme de ''fa'' mineur, mais une harmonie sur la gamme de ''fa'' majeur. La grille d'accord comporte les accords construits sur les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la gamme majeure homonyme. Les accords sont souvent des accords de septième (donc avec une tierce majeure et une septième mineure), il ne s'agit donc pas d'une harmonisation de gamme diatonique (puisque la septième est majeure sur l'accord de tonique). Par exemple, pour un blues en ''do'' : * accord parfait de do majeur, C ({{Times New Roman|I}}<sup>er</sup> degré) ; * accord parfait de fa majeur, F ({{Times New Roman|IV}}<sup>e</sup> degré) ; * accord parfait de sol majeur, G ({{Times New Roman|V}}<sup>e</sup> degré). Il existe quelques morceaux harmonisés avec des accords mineurs, comme par exemple ''As the Years Go Passing By'' d'Albert King (Duje Records, 1959). La progression blues est organisée en trois blocs de quatre mesures ayant les fonctions suivantes (voir ci-dessus ''[[#Harmonie fonctionnelle|Harmonie fonctionnelle]]'') : * quatre mesures toniques ; * quatre mesures sous-dominantes ; * quatre mesures dominantes. La forme la plus simple, que Jeff Gardner appelle « forme A », est la suivante : {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, forme A |- ! scope="row" | Tonique | width="50px" | I | width="50px" | I | width="50px" | I | width="50px" | I |- ! scope="row" | Sous-domminante | width="50px" | IV | width="50px" | IV | width="50px" | I | width="50px" | I |- ! scope="row" | Dominante | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | V |} La progression {{Times New Roman|I-V}} des deux dernières mesures forment le ''{{lang|en|turnaround}}'', la demie cadence qui lance le cycle suivant. Nous présentons ci-dessous un exemple typique de ligne de basse ''({{lang|en|walking bass}})'' pour le ''{{lang|en|turnaround}}'' d'un blues en ''la'' : [[Fichier:Turnaround classique blues en la.svg|Exemple typique de ligne de basse pour un ''turnaround'' de blues en ''la''.]] [[Fichier:Blues mi harmonie elementaire.midi|thumb|Blues en ''mi'', harmonisé de manière élémentaire avec une ''{{lang|en|walking bass}}''.]] Vous pouvez écouter ci-contre une harmonisation typique d'un blues en ''mi''. Les accords sont exécutés par une basse marchante ''({{lang|en|walking bass}})'', qui joue une arpège sur la triade avec l'ajout d'une sixte majeure et d'une septième mineure, et par une guitare qui joue un accord de puissance ''({{lang|en|power chord}})'', qui n'est composé que de la fondamentale et de la quinte juste, avec une sixte en appoggiature. La forme B s'obtient en changeant la deuxième mesure : on joue un degré {{Times New Roman|IV}} au lieu d'un degré {{Times New Roman|I}}. La progression {{Times New Roman|I-IV}} sur les deux premières mesures est appelé ''{{lang|en|quick change}}''. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, forme B |- | width="50px" | I | width="50px" | IV | width="50px" | I | width="50px" | I |- | width="50px" | IV | width="50px" | IV | width="50px" | I | width="50px" | I |- | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | V |} Par exemple, ''Sweet Home Chicago'' (Robert Johnson, 1936) est un blues en ''fa'' ; sa grille d'accords, aux variations près, suit une forme B : {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression de ''Sweet Home Chicago'' |- | width="50px" | F | width="50px" | B♭ | width="50px" | F | width="50px" | F |- | width="50px" | B♭ | width="50px" | B♭ | width="50px" | F | width="50px" | F |- | width="50px" | C7 | width="50px" | B♭7 | width="50px" | F7 | width="50px" | C7 |} : Écouter {{lien web | url =https://www.youtube.com/watch?v=dkftesK2dck | titre = Robert Johnson "Sweet Home Chicago" | auteur = Michal Angel | site = YouTube | date = 2007-12-09 | consulté le = 2020-12-17 }} Les formes C et D s'obtiennent à partir des formes A et B en changeant le dernier accord par un accord sur le degré {{Times New Roman|I}}, ce qui forme une cadence plagale. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, formes C et D |- | colspan="4" | … |- | colspan="4" | … |- | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | I |} L'harmonie peut être enrichie, notamment en jazz. Voici par exemple une grille du blues souvent utilisés en bebop. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Exemple de progression de blues bebop sur une base de forme B |- | width="60px" | I<sup>7</sup> | width="60px" | IV<sup>7</sup> | width="60px" | I<sup>7</sup> | width="60px" | V–<sup>7</sup> <nowiki>|</nowiki> I<sup>7</sup> |- | width="60px" | IV<sup>7</sup> | width="60px" | IV<sup>7</sup> | width="60px" | I<sup>7</sup> | width="60px" | VI<sup>7 ♯9 ♭13</sup> |- | width="60px" | II–<sup>7</sup> | width="60px" | V<sup>7</sup> | width="60px" | V<sup>7</sup> <nowiki>|</nowiki> IV<sup>7</sup> | width="60px" | II–<sup>7</sup> <nowiki>|</nowiki> V<sup>7</sup> |} On peut aussi trouver des blues sur huit mesures, sur seize mesures comme ''Watermelon Man'' de Herbie Hancock (album ''Takin' Off'', Blue Note, 1962) ou ''Let's Dance'' de Jim Lee (interprété par Chris Montez, Monogram, 1962) * {{lien web |url= https://www.dailymotion.com/video/x5iduwo |titre=Herbie Hancock - Watermelon Man (1962) |auteur=theUnforgettablesTv |site=Dailymotion |date=2003 |consulté le=2021-02-09 }} * {{lien web |url=https://www.youtube.com/watch?v=6JXshurYONc |titre=Let's Dance |auteur=Chris Montez |site=YouTube |date=2016-08-06 |consulté le=2021-02-09 }} À l'inverse, certains blues peuvent avoir une structure plus simple que les douze mesure ; par exemple ''Hoochie Coochie Man'' de Willie Dixon (interprété par Muddy Waters sous le titre ''Mannish Boy'', Chicago Blues, 1954) est construit sur un seul accord répété tout le long de la chanson. * {{lien web |url=https://www.dailymotion.com/video/x5iduwo |titre=Muddy Waters - Hoochie Coochie Man |auteur=Muddy Waters |site=Dailymotion |date=2012 | consulté le=2021-02-09 }} === Cadence andalouse === La cadence andalouse est une progression de quatre accords, descendant par mouvement conjoint : * en mode de ''mi'' (mode phrygien) : {{Times New Roman|IV}} - {{Times New Roman|III}} - {{Times New Roman|II}} - {{Times New Roman|I}} ;<br />par exemple en ''mi'' phrygien : Am - G - F - E ; en ''do'' phrygien : Fm - E♭ - D♭ - C ;<br />on notera que le degré {{Times New Roman|III}} est diésé dans l'accord final (ou bécarre s'il est bémol dans la tonalité) ; * en mode mineur : {{Times New Roman|I}} - {{Times New Roman|VII}} - {{Times New Roman|VI}} - {{Times New Roman|V}} ;<br />par exemple en ''la'' mineur : Am - G - F - E ; en ''do'' mineur : Cm - B♭ - A♭ - m ;<br />comme précédemment, on notera que le degré {{Times New Roman|VII}} est diésé dans l'accord final. === Progressions selon le cercle des quintes === [[Fichier:Cercle quintes degres tonalite majeure.svg|vignette|Cercle des quinte justes (parcouru dans le sens des aiguilles d'une montre) des degrés d'une tonalité majeure.]] La progression {{Times New Roman|V-I}} est la cadence parfaite, mais on peut aussi l'employer au milieu d'un morceau. Cette progression étant courte, sa répétition crée de la lassitude ; on peut la compléter par d'autres accords séparés d'une quinte juste, en suivant le « cercle des quintes » : {{Times New Roman|I-V-IX}}, la neuvième étant enharmonique de la seconde, on obtient {{Times New Roman|I-V-II}} ; en retournant la progression, on obtient le turnaround {{Times New Roman|II-V-I}}. En jazz, dans le morceau ''Topsy'' (Edgar Battle et Eddir Durham), la partie <span style="border-color: black; border-style: solid; border-width: thin;">C</span> est composée d'une progression en quintes descendantes d'accords de septième de dominante : D<sup>7</sup> - G<sup>7</sup> - C<sup>7</sup> - F<sup>7</sup> - B♭<sup>7</sup> ; les improvisatiosn sur cette partie peuvent donc se faire en modulant en ''sol'' (majeur ou mineur) - ''do'' (M/m) - ''fa'' (M/m) - ''si''♭ (M/m) - ''mi''♭ (M/m). On peut continuer de décrire le cercle des quintes : {{Times New Roman|I-V-II-VI}}, on obtient l'anatole dans le désordre ; on peut à l'inverse étendre les quintes vers la gauche, {{Times New Roman|IV-I-V-II-VI}}. En musique populaire, on trouve fréquemment une progression fondée sur les accord {{Times New Roman|I}}, {{Times New Roman|IV}}, {{Times New Roman|V}} et {{Times New Roman|VI}}, popularisée dans les années 1950. La « progression années 1950 », « progression ''{{lang|en|fifties ('50)}}'' » ''({{lang|en|'50s progression}})'' est dans l'ordre {{Times New Roman|I-VI-IV-V}}. On trouve aussi cette progression en musique classique. Si la tonalité est majeure, la triade sur la sus-dominante est mineure, les autres sont majeures, on notera donc souvent {{Times New Roman|I-vi-IV-V}}. On peut avoir des permutations circulaires (le dernier accord venant au début, ou vice-versa) : {{Times New Roman|vi-IV-V-I}}, {{Times New Roman|IV-V-I-vi}} et {{Times New Roman|V-I-vi-IV}}. {| class="wikitable" |+ Accords selon la tonalité ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" style="font-family:Times New Roman" | I ! scope="col" style="font-family:Times New Roman" | IV ! scope="col" style="font-family:Times New Roman" | V ! scope="col" style="font-family:Times New Roman" | vi |- |''Do'' majeur || || C || F || G || Am |- |''Sol'' majeur || ''fa''♯ || G || C || D || Em |- |''Ré'' majeur || ''fa''♯, ''do''♯ || D || G || A || Bm |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A || D || E || F♯m |- | ''Fa'' majeur || ''si''♭ || F || B♭ || C || Dm |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭ || E♭ || F || Gm |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭ || A♭ || B♭ || Cm |} Par exemple, en tonalité de ''do'' majeur, la progression {{Times New Roman|I-vi-IV-V}} sera C-Am-F-G. Il existe d'autres progressions utilisant ces accords mais dans un autre ordre, typiquement {{Times New Roman|I–IV–vi–V}} ou une de ses permutations circulaires : {{Times New Roman|IV–vi–V-I}}, {{Times New Roman|vi–V-I-IV}} ou {{Times New Roman|V-I-IV-vi}}. Ou dans un autre ordre. PV Nova l'illustre dans plusieurs de ses « expériences » dans la version {{Times New Roman|vi-V-IV-I}}, soit Am-G-F-C, ou encore {{Times New Roman|vi-IV-I-V}}, soit Am-F-C-G : : {{lien web | url = https://www.youtube.com/watch?v=w08LeZGbXq4 | titre = Expérience n<sup>o</sup> 6 — La Happy Pop | auteur = PV Nova | site = YouTube | date = 2011-08-20 | consulté le = 2020-12-13 }} et cela devient un gag récurrent avec son « chapeau des accords magiques qu'on nous ressort à toutes les sauces » : {{lien web | url = https://www.youtube.com/watch?v=VMY_vc4nZAU | titre = Expérience n<sup>o</sup> 14 — La Soupe dou Brasil | auteur = PV Nova | site = YouTube | date = 2012-10-03 | consulté le = 2020-12-17 }} Cette récurrence est également parodiée par le groupe The Axis of Awesome avec ses « chansons à quatre accords » ''({{lang|en|four-chords song}})'', dans une sketch où ils mêlent 47 chansons en utilisant l'ordre {{Times New Roman|I-V-vi-IV}} : : {{lien web | url = https://www.youtube.com/watch?v=oOlDewpCfZQ | titre = 4 Chords | Music Videos | The Axis Of Awesome | auteur = The Axis of Awesome | site = YouTube | date = 2011-07-20 | consulté le = 2020-12-17 }} {{boîte déroulante/début|titre=Chansons mêlées dans le sketch}} # Journey : ''Don't Stop Believing'' ; # James Blunt : ''You're Beautiful'' ; # Black Eyed Peas : ''Where Is the Love'' ; # Alphaville : ''Forever Young'' ; # Jason Mraz : ''I'm Yours'' ; # Train : ''Hey Soul Sister'' ; # The Calling : ''Wherever You Will Go'' ; # Elton John : ''Can You Feel The Love Tonight'' (''Le Roi lion'') ; # Akon : ''Don't Matter'' ; # John Denver : ''Take Me Home, Country Roads'' ; # Lady Gaga : ''Paparazzi'' ; # U2 : ''With Or Without You'' ; # The Last Goodnight : ''Pictures of You'' ; # Maroon Five : ''She Will Be Loved'' ; # The Beatles : ''Let It Be'' ; # Bob Marley : ''No Woman No Cry'' ; # Marcy Playground : ''Sex and Candy'' ; # Men At Work : ''Land Down Under'' ; # thème de ''America's Funniest Home Videos'' (équivalent des émissions ''Vidéo Gag'' et ''Drôle de vidéo'') ; # Jack Johnson : ''Taylor'' ; # Spice Girls : ''Two Become One'' ; # A Ha : ''Take On Me'' ; # Green Day : ''When I Come Around'' ; # Eagle Eye Cherry : ''Save Tonight'' ; # Toto : ''Africa'' ; # Beyonce : ''If I Were A Boy'' ; # Kelly Clarkson : ''Behind These Hazel Eyes'' ; # Jason DeRulo : ''In My Head'' ; # The Smashing Pumpkins : ''Bullet With Butterfly Wings'' ; # Joan Osborne : ''One Of Us'' ; # Avril Lavigne : ''Complicated'' ; # The Offspring : ''Self Esteem'' ; # The Offspring : ''You're Gonna Go Far Kid'' ; # Akon : ''Beautiful'' ; # Timberland featuring OneRepublic : ''Apologize'' ; # Eminem featuring Rihanna : ''Love the Way You Lie'' ; # Bon Jovi : ''It's My Life'' ; # Lady Gaga : ''Pokerface'' ; # Aqua : ''Barbie Girl'' ; # Red Hot Chili Peppers : ''Otherside'' ; # The Gregory Brothers : ''Double Rainbow'' ; # MGMT : ''Kids'' ; # Andrea Bocelli : ''Time To Say Goodbye'' ; # Robert Burns : ''Auld Lang Syne'' ; # Five for fighting : ''Superman'' ; # The Axis of Awesome : ''Birdplane'' ; # Missy Higgins : ''Scar''. {{boîte déroulante/fin}} Vous pouvez par exemple jouer les accords C-G-Am-F ({{Times New Roman|I-V-vi-IV}}) et chanter dessus ''{{lang|en|Let It Be}}'' (Paul McCartney, The Beattles, 1970) ou ''Libérée, délivrée'' (Robert Lopez, ''La Reine des neiges'', 2013). La progression {{Times New Roman|I-V-vi-IV}} est considérée comme « optimiste » tandis que sa variante {{Times New Roman|iv-IV-I-V}} est considérée comme « pessimiste ». On peut voir la progression {{Times New Roman|I-vi-IV-V}} comme une variante de l'anatole {{Times New Roman|I-vi-ii-V}}, obtenue en remplaçant l'accord de sustonique {{Times New Roman|ii}} par l'accord de sous-dominante {{Times New Roman|IV}} (son relatif majeur, et degré ayant la même fonction). ==== Exemples de progression selon le cercle des quintes en musique classique ==== [[Fichier:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.midi|vignette|Dietrich Buxtehude, Psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]] Cette progression selon la cercle des quintes, sous la forme {{Times New Roman|I-vi-IV-V}}, apparaît déjà au {{pc|xvii}}<sup>e</sup> siècle dans le psaume 42 ''Quem ad modum desiderat cervis'' (BuxVW92) de Dietrich Buxtehude (1637-1707). Le morceau est en ''fa'' majeur, la progression d'accords est donc F-Dm-B♭-C. : {{lien web | url = https://www.youtube.com/watch?v=8FmV9l1RqSg | titre = D. Buxtehude - Quemadmodum desiderat cervus, BuxWV 92 | auteur = Longobardo | site = YouTube | date = 2013-04-06 | consulté la = 2021-01-01 }} [[File:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.svg|vignette|450x450px|center|Dietrich Buxtehude, psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]] {{clear}} [[Fichier:JSBach BWV140 cantate 4 mesures.midi|vignette|J.-S. Bach, cantate BWV140, quatre premières mesures.]] On la trouve également dans l'ouverture de la cantate ''{{lang|de|Wachet auf, ruft uns die Stimme}}'' de Jean-Sébastien Bach (BWV140, 1731). Le morceau est en ''mi''♭ majeur, la progression d'accords est donc E♭-Cm-A♭<sup>6</sup>-B♭. [[Fichier:JSBach BWV140 cantate 4 mesures.svg|vignette|center|J.-S. Bach, cantate BWV140, quatre premières mesures.|alt=|517x517px]] {{clear}} [[Fichier:Mozart K310 Sonate8 mesures 45 a 49.midi|vignette|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]] La même progression est utilisée par Mozart, par exemple dans le premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778), la progression d'accords est C-Am-F-G qui correspond à la progression {{Times New Roman|III-i-VI-VII}} de ''la'' mineur, mais à la progression {{Times New Roman|I-vi-IV-V}} de la gamme relative, ''do'' majeur . [[Fichier:Mozart K310 Sonate8 mesures 45 a 49.svg|vignette|center|500px|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]] === Substitution tritonique === Un des accords les plus utilisés est donc l'accord de septième de dominante, {{Times New Roman|V<sup>7</sup><sub>+</sub>}} qui contient les degrés {{Times New Roman|V}}, {{Times New Roman|VII}}, {{Times New Roman|II}} ({{Times New Roman|IX}}) et {{Times New Roman|IV}}({{Times New Roman|XI}}) ; par exemple, en tonalité de ''do'' majeur, l'accord de ''sol'' septième (G<sup>7</sup>) contient les notes ''sol''-''si''-''ré''-''fa''. Si l'on prend l'accord dont la fondamentale est trois tons (triton) au-dessus ou en dessous — l'octave contenant six tons, on arrive sur la même note —, {{Times New Roman|♭II<sup>7</sup>}}, ici ''ré''♭ septième (D♭<sup>7</sup>), celui-ci contient les notes ''ré''♭-''fa''-''la''♭-''do''♭, cette dernière note étant l'enharmonique de ''si''. Les deux accords G<sup>7</sup> et D♭<sup>7</sup> ont donc deux notes en commun : le ''fa'' et le ''si''/''do''♭. Il est donc fréquent en jazz de substituer l'accord {{Times New Roman|V<sup>7</sup><sub>+</sub>}} par l'accord {{Times New Roman|♭II<sup>7</sup>}}. Par exemple, la progression {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|V<sup>7</sup>}}-{{Times New Roman|I<sup>&Delta;</sup>}} devient {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|♭II<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}}. C'est un procédé courant de réharmonisation (le fait de remplacer un accord par un autre dans un morceau existant). Les six substitutions possibles sont donc : C<sup>7</sup>↔F♯<sup>7</sup> - D♭<sup>7</sup>↔G<sup>7</sup> - D<sup>7</sup>↔A♭<sup>7</sup> - E♭<sup>7</sup>↔A<sup>7</sup> - E<sup>7</sup>↔B♭<sup>7</sup> - F<sup>7</sup>↔B<sup>7</sup>. [[Fichier:Übermäsiger Terzquartakkord.jpg|vignette|Exemple de cadence parfaite en ''do'' majeur avec substitution tritonique (sixte française).]] Dans l'accord D♭<sup>7</sup>, si l'on remplace le ''do''♭ par son ''si'' enharmonique, on obtient un accord de sixte augmentée : ''ré''♭-''fa''-''la''♭-''si''. Cet accord est utilisé en musique classique depuis la Renaissance ; on distingue en fait trois accords de sixte augmentée : * sixte française ''ré''♭-''fa''-''sol''-''si'' ; * sixte allemande : ''ré''♭-''fa''-''la''♭-''si'' ; * sixte italienne : ''ré''♭-''fa''-''si''. Par exemple, le ''Quintuor en ''ut'' majeur'' de Franz Schubert (1828) se termine par une cadence parfaite dont l'accord de dominante est remplacé par une sixte française ''ré''♭-''fa''-''si''-''sol''-''si'' (''ré''♭ aux violoncelles, ''fa'' à l'alto, ''si''-''sol'' aux seconds violons et ''si'' au premier violon). [[Fichier:Schubert C major Quintet ending.wav|vignette|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]] [[Fichier:Schubert C major Quintet ending.png|vignette|center|upright=2.5|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]] === Autres accords de substitution === Substituer un accord consiste à utiliser un accord provenant d'une tonalité étrangère à la tonalité en cours. À la différence d'une modulation, la substitution est très courte et ne donne pas l'impression de changer de tonalité ; on a juste un sentiment « étrange » passager. Un court passage dans une autre tonalité est également appelée « emprunt ». Nous avons déjà vu plusieurs méthodes de substitution : * utilisation d'une note étrangère : une note étrangère — note de passage, appoggiature, anticipation, retard… — crée momentanément un accord hors tonalité ; en musique classique, ceci n'est pas considéré comme un accord en propre, mais en jazz, on parle « d'accord de passage » et « d'accord suspendu » ; * utilisation d'une dominante secondaire : l'accord de dominante secondaire est hors tonalité ; le but ici est de faire une cadence parfaite, mais sur un autre degré que la tonique de la tonalité en cours ; * la substitution tritonique, vue ci-dessus, pour remplacer un accord de septième de dominante. Une dernière méthode consiste à remplacer un accord par un accord d'une gamme de même tonique, mais d'un autre mode ; on « emprunte » ''({{lang|en|borrow}})'' l'accord d'un autre mode. Par exemple, substituer un accord de la tonalité de ''do'' majeur par un accord de la tonalité de ''do'' mineur ou de ''do'' mode de ''mi'' (phrygien). Donc en ''do'' majeur, on peut remplacer un accord de ''ré'' mineur septième (D<sub>m</sub><sup>7</sup>) par un accord de ''ré'' demi-diminué (D<sup>⌀</sup>, D<sub>m</sub><sup>7&flat;5</sup>) qui est un accord appartenant à la donalité de ''la'' mineur harmonique. === Forme AABA === La forme AABA est composée de deux progressions de huit mesures, notées A et B ; cela représente trente-deux mesures au total, on parle donc souvent en anglais de la ''{{lang|en|32-bars form}}''. C'est une forme que l'on retrouve dans de nombreuses chanson de comédies musicales de Broadway comme ''Have You Met Miss Jones'' (''{{lang|en|I'd Rather Be Right}}'', 1937), ''{{lang|en|Over the Rainbow}}'' (''Le Magicien d'Oz'', Harold Harlen, 1939), ''{{lang|en|All the Things You Are}}'' (''{{lang|en|Very Warm for may}}'', 1939). Par exemple, la version de ''{{lang|en|Over the Rainbow}}'' chantée par Judy Garland est en ''la''♭ majeur et la progression d'accords est globalement : * A (couplet) : A♭-Fm | Cm-A♭ | D♭ | Cm-A♭ | D♭ | D♭-F | B♭-E♭ | A♭ * B (pont) : A♭ | B♭m | Cm | D♭ | A♭ | B♭-G | Cm-G | B♭m-E♭ soit en degrés : * A : {{Times New Roman|<nowiki>I-vi | iii-I | IV | iii-IV | IV | IV-vi | II-V | I</nowiki>}} * B : {{Times New Roman|<nowiki>I | ii | iii | IV | I | II-VII | iii-VII | ii-V</nowiki>}} Par rapport aux paroles de la chanson, on a * A : couplet 1 ''« {{lang|en|Somewhere […] lullaby}} »'' ; * A : couplet 2 ''« {{lang|en|Somewhere […] really do come true}} »'' ; * B : pont ''« {{lang|en|Someday […] you'll find me}} »'' ; * A : couplet 3 ''« {{lang|en|Somewhere […] oh why can't I?}} »'' ; : {{lien web | url = https://www.youtube.com/watch?v=1HRa4X07jdE | titre = Judy Garland - Over The Rainbow (Subtitles) | site = YouTube | auteur = Overtherainbow | consulté le = 2020-12-17 }} Une mise en œuvre de la forme AABA couramment utilisée en jazz est la forme anatole (à le pas confondre avec la succession d'accords du même nom), en anglais ''{{lang|en|rythm changes}}'' car elle s'inspire du morceau ''{{lang|en|I Got the Rythm}}'' de George Gerschwin (''Girl Crazy'', 1930) : * A : {{Times New Roman|I–vi–ii–V}} (succession d'accords « anatole ») ; * B : {{Times New Roman|III<sup>7</sup>–VI<sup>7</sup>–II<sup>7</sup>–V<sup>7</sup>}} (les fondamentales forment une succession de quartes, donc parcourent le « cercle des quintes » à l'envers). Par exemple, ''I Got the Rythm'' étant en ''ré''♭ majeur, la forme est : * A : D♭ - B♭m - E♭m - A♭ * B : F7 - B♭7 - E♭7 - A♭7 === Exemples === ==== Début du Largo de la symphonie du Nouveau Monde ==== [[File:Largo nouveau monde 5 1res mesures.svg|vignette|Partition avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]] [[File:Largo nouveau monde 5 1res mesures.midi|vignette|Fichier son avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]] Nous avons reproduit ci-contre les cinq premières mesure du deuxième mouvement Largo de la symphonie « Du Nouveau Monde » (symphonie n<sup>o</sup> 9 d'Antonín Dvořák, 1893). Cliquez sur l'image pour l'agrandir. Vous pouvez écouter cette partie jouée par un orchestre symphonique : * {{lien web |url =https://www.youtube.com/watch?v=y2Nw9r-F_yQ?t=565 |titre = Dvorak Symphony No.9 "From the New World" Karajan 1966 |site=YouTube (Seokjin Yoon) |consulté le=2020-12-11 }} (à 9 min 25), par le Berliner Philharmoniker, dirigé par Herbert von Karajan (1966) ; * {{lien web |url = https://www.youtube.com/watch?v=ASlch7R1Zvo |titre=Dvořák: Symphony №9, "From The New World" - II - Largo |site=YouTube (diesillamusicae) |consulté le=2020-12-11 }} : Wiener Philharmoniker, dirigé par Herbert von Karajan (1985). {{clear}} Cette partie fait intervenir onze instruments monodiques (ne jouant qu'une note à la fois) : des vents (trois bois, sept cuivres) et une percussion. Certains de ces instruments sont transpositeurs (les notes sur la partition ne sont pas les notes entendues). Jouées ensemble, ces onze lignes mélodiques forment des accords. Pour étudier cette partition, nous réécrivons les parties des instruments transpositeurs en ''do'' et les parties en clef d’''ut'' en clef de ''fa''. Nous regroupons les parties en clef de ''fa'' d'un côté et les parties en clef de ''sol'' d'un autre. {{boîte déroulante|Résultat|contenu=[[File:Largo nouveau monde 5 1res mesures transpositeurs en do.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde, en do.]]}} Nous pouvons alors tout regrouper sous la forme d'un système de deux portées clef de ''fa'' et clef de ''sol'', comme une partition de piano. {{boîte déroulante|Résultat|contenu= [[Fichier:Largo nouveau monde 5 1res mesures accords.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords.]] }} [[Fichier:Largo nouveau monde 5 1res mesures accords.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords.]] {{clear}} Ensuite, nous ne gardons que la basse et les notes médium. Nous changeons éventuellement certaines notes d'octave afin de n'avoir que des superpositions de tierce ou de quinte (état fondamental des accords, en faisant ressortir les notes manquantes). {{boîte déroulante|Résultat|contenu= [[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords simplifiés.]] }} [[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords simplifiés.]] Vous pouvez écouter cette partie jouée par un quintuor de cuivres (trompette, bugle, cor, trombone, tuba), donc avec des accords de cinq notes : : {{lien web |url=https://www.youtube.com/watch?v=pWfe60nbvjA |titre = Largo from The New World Symphony by Dvorak |site=YouTube (The Chamberlain Brass) |consulté le=2020-12-11 }} : The American Academy of Arts & Letters in New York City (2017). Nous allons maintenant chiffrer les accords. Pour établir la basse chiffrée, il nous faut déterminer le parcours harmonique. Pour le premier accord, les tonalités les plus simples avec un ''sol'' dièse sont ''la'' majeur et ''fa'' dièse mineur ; comme le ''mi'' est bécarre, nous retenons ''la'' majeur, il s'agit donc d'un accord de quinte sur la dominante (les accords de dominante étant très utilisés, cela nous conforte dans notre choix). Puis nous avons un ''si'' bémol, nous pouvons être en ''fa'' majeur ou en ''ré'' mineur ; nous retenons ''fa'' majeur, c'est donc le renversement d'un accord sur le degré {{Times New Roman|II}}. Dans la deuxième mesure, nous revenons en ''la'' majeur, puis, avec un ''la'' et un ''ré'' bémols, nous sommes en ''la'' bémol majeur ; nous avons donc un accord de neuvième incomplet sur la sensible, ou un accord de onzième incomplet sur la dominante. Dans la troisième mesure, nous passons en ''ré'' majeur, avec un accord de dominante. Puis, nous arrivons dans la tonalité principale, avec le renversement d'un accord de dominante sans tierce suivi d'un accord de tonique. Nous avons donc une cadence parfaite, conclusion logique d'une phrase. La progression des accords est donc : {| class="wikitable" ! scope="row" | Tonalité | ''la'' M - ''fa'' M || ''la'' M - ''la''♭ M || ''ré'' M - ''ré''♭ M || ''ré''♭ M |- ! scope="row" | Accords | {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|II}}<sup>6</sup><sub>4</sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|“V”}}<sup>9</sup><sub><s>5</s></sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|V}}<sup>+4</sup> || {{Times New Roman|I}}<sup>5</sup> |} Dans le chiffrage jazz, nous avons donc : * une triade de ''mi'' majeur, E ; * une triade de ''sol'' majeur avec un ''ré'' en basse : G/D ; * à nouveau un E ; * un accord de ''sol'' neuvième diminué incomplet, avec un ''ré'' bémol en basse : G dim<sup>9</sup>/D♭ ; * un accord de ''la'' majeur, A ; * un accord de ''la'' bémol septième avec une ''sol'' bémol à la basse : A♭<sup>7</sup>/G♭ ; * la partie se conclue par un accord parfait de ''ré''♭ majeur, D♭. Soit une progression E - G/D | E - G dim<sup>9</sup>/D♭ | A - A♭<sup>7</sup>/G♭ | D♭. [[Fichier:Largo nouveau monde 5 1res mesures accords chiffres.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde en accords simplifiés.]] {{clear}} ==== Thème de Smoke on the Water ==== Le morceau ''Smoke on the Water'' du groupe Deep Purple (album ''Machine Head'', 1972) possède un célèbre thème, un riff ''({{lang|en|rythmic figure}})'', joué à la guitare sous forme d'accords de puissance ''({{lang|en|power chords}})'', c'est-à-dire des accords sans tierce. Le morceau est en tonalité de ''sol'' mineur naturel (donc avec un ''fa''♮) avec ajout de la note bleue (''{{lang|en|blue note}}'', quinte diminuée, ''ré''♭), et les accords composant le thème sont G<sup>5</sup>, B♭<sup>5</sup>, C<sup>5</sup> et D♭<sup>5</sup>, ce dernier accord étant l'accord sur la note bleue et pouvant être considéré comme une appoggiature (indiqué entre parenthèse ci-après). On a donc ''a priori'', sur les deux premières mesures, une progression {{Times New Roman|I-III-IV}} puis {{Times New Roman|I-III-(♭V)-IV}}. Durant la majeure partie du thème, la guitare basse tient la note ''sol'' en pédale. {{note|En jazz, la qualité « <sup>5</sup> » indique que l'on n'a que la quinte (et donc pas la tierce), contrairement à la notation de basse chiffrée.}} : {{lien web | url = https://www.dailymotion.com/video/x5ili04 | titre = Deep Purple — Smoke on the Water (Live at Montreux 2006) | auteur = Deep Purple | site = Dailymotion | date = 2016 | consulté le = 2020-12-31 }} Cependant, cette progression forme une mélodie, on peut donc plus la voir comme un contrepoint, la superposition de deux voies ayant un mouvement conjoint, joué par un seul instrument, la guitare, la voie 2 étant jouée une quarte juste en dessous de la voie 1 (la quarte juste descendante étant le renversement de la quinte juste ascendante) : * voie 1 (aigu) : | ''sol'' - ''si''♭ - ''do'' | ''sol'' - ''si''♭ - (''ré''♭) - ''do'' | ; * voie 2 (grave) : | ''ré'' - ''fa'' - ''sol'' | ''ré'' - ''fa'' - (''la''♭) - ''sol'' |. En se basant sur la basse (''sol'' en pédale), nous pouvons considérer que ces deux mesures sont accompagnées d'un accord de Gm<sup>7</sup> (''sol''-''si''♭-''ré''-''fa''), chaque accord de la mélodie comprenant à chaque fois au moins une note de cet accord à l'exception de l'appogiature. {| class="wikitable" |+ Mise en évidence des notes de l'accord Gm<sup>7</sup> |- ! scope="row" | Accords | G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> |- ! scope="row" | Voie 1 | '''''sol''''' || '''''si''♭''' || ''do'' |- ! scope="row" | Voie 2 | '''''ré''''' || '''''fa''''' || '''''sol''''' |- ! scope="row" | Basse | '''''sol''''' || '''''sol''''' || '''''sol''''' |} Sur les deux mesures suivantes, la basse varie et suit les accords de la guitare avec un retard sur le dernier accord : {| class="wikitable" |+ Voies sur les mesure 3-4 du thème |- ! scope="row" | Accords | G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> || B♭<sup>5</sup> || G<sup>5</sup> |- ! scope="row" | Voie 1 | ''sol'' || ''si''♭ || ''do'' || ''si''♭ || ''sol'' |- ! scope="row" | Voie 2 | ''ré'' || ''fa'' || ''sol'' || ''fa'' || ''ré'' |- ! scope="row" | Basse | ''sol'' || ''sol'' || ''do'' || ''si''♭ || ''si''♭-''sol'' |} Le couplet de cette chanson est aussi organisé sur une progression de quatre mesures, la guitare faisant des arpèges sur les accords G<sup>5</sup> (''sol''-''ré''-''sol'') et F<sup>5</sup> (''fa''-''do''-''fa'') : : | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-F<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | soit une progression {{Times New Roman|<nowiki>| I-I | I-I | I-VII | I-I |</nowiki>}}. Nous pouvons aussi harmoniser le riff du thème sur cette progression, avec un accord F (''fa''-''la''-''do'') ; nous pouvons aussi nous rappeler que l'accord sur le degré {{Times New Roman|VII}} est plus volontiers considéré comme un accord de septième de dominante {{Times New Roman|V<sup>7</sup>}}, soit ici un accord Dm<sup>7</sup> (''ré''-''fa''-''la''-''do''). On peut donc considérer la progression harmonique sur le thème : : | Gm-Gm | Gm-Gm | Gm-F ou Dm<sup>7</sup> | Gm-Gm |. Cette analyse permet de proposer une harmonisation enrichie du morceau, tout en se rappelant qu'une des forces du morceau initial est justement la simplicité de sa structure, qui fait ressortir la virtuosité des musiciens. Nous pouvons ainsi comparer la version album à la version concert avec orchestre ou à la version latino de Pat Boone. À l'inverse, le groupe Psychostrip, dans une version grunge, a remplacé les accords par une ligne mélodique : * le thème ne contient plus qu'une seule voie (la guitare ne joue pas des accords de puissance) ; * dans les mesures 9 et 10, la deuxième guitare joue en contrepoint de type mouvement inverse, qui est en fait la voie 2 jouée en miroir ; * l'arpège sur le couplet est remplacé par une ligne mélodique en ostinato sur une gamme blues. {| class="wikitable" |+ Contrepoint sur les mesures 9 et 10 |- ! scope="row" | Guitare 1 | ''sol'' ↗ ''si''♭ ↗ ''do'' |- ! scope="row" | Guitare 2 | ''sol'' ↘ ''fa'' ↘ ''ré'' |} * {{lien web | url = https://www.dailymotion.com/video/x5ik234 | titre = Deep Purple — Smoke on the Water (In Concert with the London Symphony Orchestra, 1999) | auteur = Deep Purple | site = Dailymotion | date = 2016 | consulté le = 2020-12-31 }} * {{lien web | url = https://www.youtube.com/watch?v=MtUuNzVROIg | titre = Pat Boone — Smoke on the Water (In a Metal Mood, No More Mr. Nice Guy, 1997) | auteur = Orrore a 33 Giri | site = YouTube | date = 2019-06-24 | consulté le = 2020-12-31 }} * {{lien web | url = https://www.youtube.com/watch?v=n7zLlZ8B0Bk | titre = Smoke on the Water (Heroes, 1993) | auteur = Psychostrip | site = YouTube | date = 2018-06-20 | consulté le = 2020-12-31 }} == Accords et improvisation == Nous avons vu précédemment (chapitre ''[[../Gammes et intervalles#Modes et improvisation|Gammes et intervalles &gt; Modes et improvisation]]'') que le choix d'un mode adapté permet d'improviser sur un accord. L'harmonisation des gammes permet, en inversant le processus, d'étendre notre palette : il suffit de repérer l'accord sur une harmonisaiton de gamme, et d'utiliser cette gamme-là, dans le mode correspondant du degré de l'accord (voir ci-dessus ''[[#Harmonisation par des accords de septième|Harmonisation par des accords de septième]]''). Par exemple, nous avons vu que l'accord sur le septième degré d'une gamme majeure était un accord demi-diminué ; nous savons donc que sur un accord demi-diminué, nous pouvons improviser sur le mode correspondant au septième degré, soit le mode de ''si'' (locrien). Un accord de septième de dominante étant commun aux deux tonalités homonymes (par exemple ''fa'' majeur et ''fa'' mineur pour un ''do''<sup>7</sup><sub>+</sub> / C<sup>7</sup>), nous pouvons utiliser le mode de ''sol'' de la gamme majeure (mixolydien) ou de la gamme mineure mineure (mode phrygien dominant, ou phrygien espagnol) pour improviser. Mais l'accord de septième de dominante est aussi l'accord au début d'une grille blues ; on peut donc improviser avec une gamme blues, même si la tierce est majeure dans l'accord et mineure dans la gamme. [[Fichier:Mode improvisation accords do complet.svg]] == Autres accords courants == [[fichier:Cluster cdefg.png|vignette|Agrégat ''do - ré - mi - fa - sol''.]] Nous avons vu précédemment l'harmonisation des tonalités majeures et mineures harmoniques par des triades et des accords de septième ; certains accords étant rarement utilisés (l'accord sur le degré {{Times New Roman|III}} et, pour les tonalités mineures harmoniques, l'accord sur la tonique), certains accords étant utilisés comme des accords sur un autre degré (les accords sur la sensible étant considérés comme des accords de dominante sans fondamentale). Dans l'absolu, on peut utiliser n'importe quelle combinaison de notes, jusqu'aux agrégats, ou ''{{lang|en|clusters}}'' (mot anglais signifiant « amas », « grappe ») : un ensemble de notes contigües, séparées par des intervalles de seconde. Dans la pratique, on reste souvent sur des accords composés de superpositions de tierces, sauf dans le cas de transitions (voir la section ''[[#Notes étrangères|Notes étrangère]]''). === En musique classique === On utilise parfois des accords dont les notes ne sont pas dans la tonalité (hors modulation). Il peut s'agir d'accords de passage, de notes étrangères, par exemple utilisant un chromatisme (mouvement conjoint par demi-tons). Outre les accords de passage, les autres accords que l'on rencontre couramment en musique classique sont les accords de neuvième, et les accords de onzième et treizième sur tonique. Ces accords sont simplement obtenus en continuant à empiler les tierces. Il n'y a pas d'accord d'ordre supérieur car la quinzième est deux octaves au-dessus de la fondamentale. Comme pour les accords de septième, on distingue les accords de neuvième de dominante et les accords de neuvième d'espèce. Dans le cas de la neuvième de dominante, il y a une différence entre les tonalités majeures et mineures : l'intervalle de neuvième est respectivement majeur et mineur. Les chiffrages des renversements peuvent donc différer. Comme pour les accords de septième de dominante, on considère que les accords de septième sur le degré {{Times New Roman|VI}} sont en fait des accords de neuvième de dominante sans fondamentale. Les accords de neuvième d'espèce sont en général préparés et résolus. Préparés : la neuvième étant une note dissonante (c'est à une octave près la seconde de la fondamentale), l'accord qui précède doit contenir cette note, mais dans un accord consonant ; la neuvième est donc commune avec l'accord précédent. Résolus : la dissonance est résolue en abaissant la neuvième par un mouvement conjoint. Par exemple, en tonalité de ''do'' majeur, si l'on veut utiliser un accord de neuvième d'espèce sur la tonique ''(do - mi - sol - si - ré)'', on peut utiliser avant un accord de dominante ''(sol - si - ré)'' en préparation puis un accord parfait sur le degré {{Times New Roman|IV}} ''(fa - la - do)'' en résolution ; nous avons donc sur la voie la plus aigüe la succession ''ré'' (consonant) - ''ré'' (dissonant) - ''do'' (consonant). On rencontre également parfois des accords de onzième et de treizième. On omet en général la tierce, car elle est dissonante avec la onzième. L'accord le plus fréquemment rencontré est l'accord sur la tonique : on considère alors que c'est un accord sur la dominante que l'on a enrichi « par le bas », en ajoutant une quinte inférieure. par exemple, dans la tonalité de ''do'' majeur, l'accord ''do - sol - si - ré - fa'' est considéré comme un accord de septième de dominante sur tonique, le degré étant noté « {{Times New Roman|V}}/{{Times New Roman|I}} ». De même pour l'accord ''do - sol - si - ré - fa - la'' qui est considéré comme un accord de neuvième de dominante sur tonique. === En jazz === En jazz, on utilise fréquemment l'accord de sixte à la place de l'accord de septième majeure sur la tonique. Par exemple, en ''do'' majeur, on utilise l'accord C<sup>6</sup> ''(do - mi - sol - la)'' à la place de C<sup>Δ</sup> ''(do - mi - sol - si)''. On peut noter que C<sup>6</sup> est un renversement de Am<sup>7</sup> et pourrait donc se noter Am<sup>7</sup>/C ; cependant, le fait de le noter C<sup>6</sup> indique que l'on a bien un accord sur la tonique qui s'inscrit dans la tonalité de ''do'' majeur (et non, par exemple, de ''la'' mineur naturelle) — par rapport à l'harmonie fonctionnelle, on remarquera que Am<sup>7</sup> a une fonction tonique, l'utilisation d'un renversement de Am<sup>7</sup> à la place d'un accord de C<sup>Δ</sup> est donc logique. Les accords de neuvième, onzième et treizième sont utilisés comme accords de septième enrichis. Le chiffrage suit les règles habituelles : on ajoute un « 9 », un « 11 » ou un « 13 » au chiffrage de l'accord de septième. On utilise également des accords dits « suspendus » : ce sont des accords de transition qui sont obtenus en prenant une triade majeure ou mineure et en remplaçant la tierce par la quarte juste (cas le plus fréquent) ou la seconde majeure. Plus particulièrement, lorsque l'on parle simplement « d'accord suspendu » sans plus de précision, cela désigne l'accord de neuvième avec une quarte suspendue, noté « 9sus4 » ou simplement « sus ». == L'harmonie tonale == L'harmonie tonale est un ensemble de règle assez strictes qui s'appliquent dans la musique savante européenne, de la période baroque à la période classique classique ({{pc|xiv}}<sup>e</sup>-{{pc|xviii}}<sup>e</sup> siècle). Certaines règles sont encore largement appliquées dans divers styles musicaux actuels, y compris populaire (rock, rap…), d'autres sont au contraire ignorées (par exemple, un enchaînement de plusieurs accords de même qualité forme un mouvement parallèle, ce qui est proscrit en harmonie tonale). De nos jours, on peut voir ces règles comme des règles « de bon goût », et leur application stricte comme une manière de composer « à la manière de ». Précédemment, nous avons vu la progression des accords. Ci-après, nous abordons aussi la manière dont les notes de l'accord sont réparties entre plusieurs voix, et comment on construit chaque voix. === Concepts fondamentaux === ; Consonance : Les intervalles sont considérés comme « plus ou moins consonants » : :* consonance parfaite : unisson, quinte et octave ; :* consonance mixte (parfaite dans certains contextes, imparfaite dans d'autres) : quarte ; :* consonance imparfaite : tierce et sixte ; :* dissonance : seconde et septième. ; Degrés : Certains degrés sont considérés comme « forts », « meilleurs », ce sont les « notes tonales » : {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (sous-dominante) et {{Times New Roman|V}} (dominante). [[Fichier:Mouvements harmoniques.svg|vignette|upright=0.75|Mouvements harmoniques.]] ; Mouvements : Le mouvement décrit la manière dont les voix évoluent les unes par rapport aux autres : :# Mouvement parallèle : les voix sont séparées par un intervalle constant. :# Mouvement oblique : une voix reste constante, c'est le bourdon ; l'autre monte ou descend. :# Mouvement contraire : une voix descend, l'autre monte. :# Échange de voix : les voix échangent de note ; les mélodies se croisent mais on a toujours le même intervalle harmonique. {{clear}} === Premières règles === ; Règle du plus court chemin : Quand on passe d'un accord à l'autre, la répartition des notes se fait de sorte que chaque voix fait le plus petit mouvement possible. Notamment : si les deux accords ont des notes en commun, alors les voix concernées gardent la même note. : Les deux voix les plus importantes sont la voix aigüe — soprano — et la voix la plus grave — basse. Ces deux voix sont relativement libres : la voix de soprano a la mélodie, la voix de basse fonde l'harmonie. La règle du plus court chemin s'applique surtout aux voix intermédiaires ; si l'on a des mouvements conjoints, ou du moins de petits intervalles — c'est le sens de la règle du plus court chemin —, alors les voix sont plus faciles à interpréter. Cette règle évite également que les voix n'empiètent l'une sur l'autre (voir la règle « éviter le croisement des voix »). ; Éviter les consonances parfaites consécutives :* Lorsque deux voix sont à l'unisson ou à l'octave, elles ne doivent pas garder le même intervalle, l'effet serait trop plat. :* Lorsque deux voix sont à la quarte ou à la quinte, elles ne doivent pas garder le même intervalle, car l'effet est trop dur. : Pour éviter cela, lorsque l'on part d'un intervalle juste, on a intérêt à pratiquer un mouvement contraire aux voix qui ne gardent pas la même note, ou au moins un mouvement direct : les voix vont dans le même sens, mais l'intervalle change. : Notez que même avec le mouvement contraire, on peut avoir des consonances parfaites consécutives, par exemple si une voix fait ''do'' aigu ↗ ''sol'' aigu et l'autre ''sol'' médium ↘ ''do'' grave. : L'interdiction des consonances parfaites consécutives n'a pas été toujours appliquée, le mouvement parallèle strict a d'ailleurs été le premier procédé utilisé dans la musique religieuse au {{pc|x}}<sup>e</sup> siècle. On peut par exemple utiliser des quintes parallèles pour donner un style médiéval au morceau. On peut également utiliser des octaves parallèles sur plusieurs notes afin de créer un effet de renforcement de la mélodie. : Par ailleurs, les consonances parfaites consécutives sont acceptées lorsqu'il s'agit d'une cadence (transition entre deux parties ou bien conclusion du morceau). ; Éviter le croisement des voix : Les voix sont organisées de la plus grave à la plus aigüe. Deux voix n'étant pas à l'unisson, celle qui est plus aigüe ne doit pas devenir la plus grave et ''vice versa''. ; Soigner la partie soprano : Comme c'est celle qu'on entend le mieux, c'est en général celle qui porte la mélodie principale. On lui applique des règles spécifiques : :# Si elle chante la sensible dans un accord de dominante ({{Times New Roman|V}}), alors elle doit monter à la tonique, c'est-à-dire que la note suivante sera la tonique située un demi-ton au dessus. :# Si l'on arrive à une quinte ou une octave entre les parties basse et soprano par un mouvement direct, alors sur la partie soprano, le mouvement doit être conjoint. On doit donc arriver à cette situation par des notes voisines au soprano. ; Préférer certains accords : Les deux degrés les plus importants sont la tonique ({{Times New Roman|I}}) et la dominante ({{Times New Roman|V}}), les accords correspondants ont donc une importance particulière. : À l'inverse, l'accord de sensible ({{Times New Roman|VII}}) n'est pas considéré comme ayant une fonction harmonique forte. On le considère comme un accord de dominante affaibli. En tonalité mineure, on évite également l'accord de médiante ({{Times New Roman|III}}). : Donc on utilise en priorité les accords de : :# {{Times New Roman|I}} et {{Times New Roman|V}}. :# Puis {{Times New Roman|II}}, {{Times New Roman|IV}}, {{Times New Roman|VI}} ; et {{Times New Roman|III}} en mode majeur. :# On évite {{Times New Roman|VII}} ; et {{Times New Roman|III}} en mode mineur. ; Préférer certains enchaînements : Les enchaînements d'accord peuvent être classés par ordre de préférence. Par ordre de préférence décroissante (du « meilleur » au « moins bon ») : :# Meilleurs enchaînements : quarte ascendante ou descendante. Notons que la quarte est le renversement de la quinte, on a donc des enchaînements stables et naturels, mais avec un intervalle plus court qu'un enchaînement de quintes. :# Bons enchaînements : tierce ascendante ou descendante. Les accords consécutifs ont deux notes en commun. :# Enchaînements médiocres : seconde ascendante ou descendante. Les accords sont voisins, mais ils n'ont aucune note en commun. On les utilise de préférence en mouvement ascendant, et on utilise surtout les enchaînements {{Times New Roman|IV}}-{{Times New Roman|V}}, {{Times New Roman|V}}-{{Times New Roman|VI}} et éventuellement {{Times New Roman|I}}-{{Times New Roman|II}}. :# Les autres enchaînements sont à éviter. : On peut atténuer l'effet d'un enchaînement médiocre en plaçant le second accord sur un temps faible ou bien en passant par un accord intermédiaire. [[Fichier:Progression Vplus4 I6.svg|thumb|Résolution d'un accord de triton (quarte sensible) vers l'accord de sixte de la tonique.]] ; La septième descend par mouvement conjoint : Dans un accord de septième de dominante, la septième — qui est donc le degré {{Times New Roman|IV}} — descend par mouvement conjoint — elle est donc suivie du degré {{Times New Roman|III}}. : Corolaire : un accord {{Times New Roman|V}}<sup>+4</sup> se résout par un accord {{Times New Roman|I}}<sup>6</sup> : on a bien un enchaînement {{Times New Roman|V}} → {{Times New Roman|I}}, et la 7{{e}} (degré {{Times New Roman|IV}}), qui est la basse de l'accord {{Times New Roman|V}}<sup>+4</sup>, descend d'un degré pour donner la basse de l'accord {{Times New Roman|I}}<sup>6</sup> (degré {{Times New Roman|III}}). {{clear}} [[Fichier:Progression I64 V7plus I5.svg|thumb|Accord de sixte et de quarte cadentiel.]] ; Un accord de sixte et quarte est un accord de passage : Le second renversement d'un accord parfait est soit une appoggiature, soit un accord de passage, soit un accord de broderie. : S'il s'agit de l'accord de tonique {{Times New Roman|I}}<sup>6</sup><sub>4</sub>, c'est « accord de sixte et quarte de cadence », l'appoggiature de l'accord de dominante de la cadence parfaite. {{clear}} Mais il faut appliquer ces règles avec discernement. Par exemple, la voix la plus aigüe est celle qui s'entend le mieux, c'est donc elle qui porte la mélodie principale. Il est important qu'elle reste la plus aigüe. La voix la plus grave porte l'harmonie, elle pose les accords, il est donc également important qu'elle reste la plus grave. Ceci a deux conséquences : # Ces deux voix extrêmes peuvent avoir des intervalles mélodiques importants et donc déroger à la règle du plus court chemin : la voix aigüe parce que la mélodie prime, la voix de basse parce que la progression d'accords prime. # Les croisements des voix intermédiaires sont moins critiques. Par ailleurs, si l'on applique strictement toutes les règles « meilleurs accords, meilleurs enchaînements », on produit un effet conventionnel, stéréotypé. Il est donc important d'utiliser les solutions « moins bonnes », « médiocres » pour apporter de la variété. Ajoutons que les renversements d'accords permettent d'avoir plus de souplesse : on reste sur le même accord, mais on enrichit la mélodie sur chaque voix. Le ''Bolero'' de Maurice Ravel (1928) brise un certain nombre de ces règles. Par exemple, de la mesure 39 à la mesure 59, la harpe joue des secondes. De la mesure 149 à la mesure 165, les piccolo jouent à la sixte, dans des mouvement strictement parallèle, ce qui donne d'ailleurs une sonorité étrange. À partir de la mesure 239, de nombreux instruments jouent en mouvement parallèles (piccolos, flûtes, hautbois, cor, clarinettes et violons). === Application === [[Fichier:Harmonisation possible de frere jacques exercice.svg|vignette|Exercice : harmoniser ''Frère Jacques''.]] Harmoniser ''Frère Jacques''. Nous considérons un morceau à quatre voix : basse, ténor, alto et soprano. La soprano chante la mélodie de ''Frère Jacques''. L'exercice consiste à proposer l'écriture des trois autres voix en respectant les règles énoncées ci-dessus. Pour simplifier, nous ajoutons les contraintes suivantes : * toutes les voix chantent des blanches ; * nous nous limitons aux accords de quinte (accords de trois sons composés d'une tierce et d'une quinte) sans avoir recours à leurs renversements (accords de sixte, accords de sixte et de quarte). Les notes à gauche de la portée indiquent la tessiture (ou ambitus), l'amplitude que peut chanter la voix. {{clear}} {{boîte déroulante/début|titre=Solution possible}} [[Fichier:Harmonisation possible de frere jacques solution.svg|vignette|Harmonisation possible de ''Frère Jacques'' (solution de l'exercice).]] Il n'y a pas qu'une solution possible. Le premier accord doit contenir un ''do''. Nous sommes manifestement en tonalité de ''do'' majeur, nous proposons de commencer par l'accord parfait de ''do'' majeur, I<sup>5</sup>. Le deuxième accord doit comporter un ''ré''. Si nous utilisons l'accord de quinte de ''ré'', nous allons créer une quinte parallèle. Nous pourrions utiliser un renversement, mais nous nous imposons de chercher un autre accord. Il peut s'agir de l'accord ''si''<sup>5</sup> ''(si-ré-fa)'' ou de l'accord de ''sol''<sup>5</sup> ''(sol-si-ré)''. La dernière solution permet d'utiliser l'accord de dominante qui est un accord important de la tonalité. La règle du plus court chemin imposerait le ''sol'' grave pour la partie de basse, mais cela est proche de la limite du chanteur, nous préférons passer au ''sol'' aigu, plus facile à chanter. Nous vérifions qu'il n'y a pas de quinte parallèle : l'intervalle ascendant ''do-sol'' (basse-alto) devient ''sol-si'' (3<sup>ce</sup>), l'intervalle descendant ''do-sol'' (soprano-alto) devient ''ré-si'' (3<sup>ce</sup>). De la même manière, pour le troisième accord, nous ne pouvons pas passer à un accord de ''la''<sup>5</sup> pour éviter une quinte parallèle. Nous avons le choix entre ''do''<sup>5</sup> ''(do-mi-sol)'' et ''mi''<sup>5</sup> ''(mi-sol-si)''. Nous préférons revenir à l'accord de fondamental, solution très stable (l'enchaînement {{Times New Roman|V}}-{{Times New Roman|I}} formant une cadence parfaite). Pour le quatrième accord, nous pourrions rester sur l'accord parfait de ''do'' mais cela planterait en quelque sorte la fin du morceau puisque l'on resterait sur la cadence parfaite ; or, nous connaissons le morceau et savons qu'il n'est pas fini. Nous choisissons l'accord de ''la''<sup>5</sup> qui est une sixte ascendante ({{Times New Roman|I}}-{{Times New Roman|VI}}). Nos aurions pu répartir les voix différemment. Par exemple : * alto : ''sol''-''si''-''sol''-''do'' ; * ténor : ''mi''-''ré''-''mi''-''mi''. {{boîte déroulante/fin}} [[Fichier:Harmonisation possible de frere jacques.midi|vignette|Fichier son correspondant.]] {{clear}} == Annexe == === Accords en musique classique === Un accord est un ensemble de notes jouées simultanément. Il peut s'agir : * de notes jouées par plusieurs instruments ; * de notes jouées par un même instrument : piano, clavecin, orgue, guitare, harpe (la plupart des instruments à clavier et des instruments à corde). Pour deux notes jouées simultanément, on parle d'intervalle « harmonique » (par opposition à l'intervalle « mélodique » qui concerne les notes jouées successivement). Les notes répétées à différentes octaves ne changent pas la nature de l'accord. La musique classique considère en général des empilements de tierces ; un accord de trois notes sera constitué de deux tierces successives, un accord de quatre notes de trois tierces… Lorsque tous les intervalles sont des intervalles impairs — tierces, quintes, septièmes, neuvièmes, onzièmes, treizièmes… — alors l'accord est dit « à l'état fondamental » (ou encore « primitif » ou « direct »). La note de la plus grave est appelée « fondamentale » de l'accord. Lorsque l'accord comporte un ou des intervalles pairs, l'accord est dit « renversé » ; la note la plus grave est appelée « basse ». De manière plus générale, l'accord est dit à l'état fondamental lorsque la basse est aussi la fondamentale. On a donc un état idéal de l'accord (état canonique) — un empilement strict de tierces — et l'état réel de l'accord — l'empilement des notes réellement jouées, avec d'éventuels redoublements, omissions et inversions ; et seule la basse indique si l'accord est à l'état fondamental ou renversé. Le chiffrage dit de « basse continue » ''({{lang|it|basso continuo}})'' désigne la représentation d'un accord sous la forme d'un ou plusieurs chiffres arabes et éventuellement d'un chiffre romain. ==== Accords de trois notes ==== En musique classique, les seuls accords considérés comme parfaitement consonants, c'est-à-dire sonnant agréablement à l'oreille, sont appelés « accords parfaits ». Si l'on prend une tonalité et un mode donné, alors l'accord construit par superposition es degrés I, III et V de cette gamme porte le nom de la gamme qui l'a généré. [[fichier:Accord do majeur chiffre.svg|vignette|upright=0.5|Accord parfait de ''do'' majeur chiffré.]] Par exemple : * « l'accord parfait de ''do'' majeur » est composé des notes ''do'', ''mi'' et ''sol'' ; * « l'accord parfait de ''la'' mineur » est composé des notes ''la'', ''do'' et ''mi''. Un accord parfait majeur est donc composé, en partant de la fondamentale, d'une tierce majeure et d'une quinte juste. Un accord parfait mineur est composé d'une tierce mineure et d'une quinte juste. L'accord parfait à l'état fondamental est appelé « accord de quinte » et est simplement chiffré « 5 » pour indiquer la quinte. On peut également commencer un accord sur sa deuxième ou sa troisième note, en faisant monter celle(s) qui précède(nt) à l'octave suivante. On parle alors de « renversement d'accord » ou d'accord « renversé ». [[Fichier:Accord do majeur renversements chiffre.svg|vignette|upright=0.75|Accord parfait de ''do'' majeur et ses renversements, chiffrés.]] Par exemple, * le premier renversement de l'accord parfait de ''do'' majeur est :<br /> ''mi'', ''sol'', ''do'' ; * le second renversement de l'accord parfait de do majeur est :<br /> ''sol'', ''do'', ''mi''. Les notes conservent leur nom de « fondamentale », « tierce » et « quinte » malgré le changement d'ordre. La note la plus grave est appelée « basse ». Dans le cas du premier renversement, le deuxième note est la tierce de la basse (la note la plus grave) et la troisième note est la sixte ; le chiffrage en chiffres arabes est donc « 6 » (puisque l'on omet la tierce) et l'accord est appelé « accord de sixte ». Pour le deuxième renversement, les intervalles sont la quarte et la sixte, le chiffrage est donc « 6-4 » et l'accord est appelé « accord de sixte et de quarte ». Dans tous les cas, on chiffre le degré on considérant la fondamentale, par exemple {{Times New Roman|I}} si l'accord est construit sur la tonique de la gamme. Les autres accords de trois notes que l'on rencontre sont : * l'accord de quinte diminuée, constitué d'une tierce mineure et d'une quinte diminuée ; lorsqu'il est construit sur le septième degré d'une gamme, on considère que c'est un accord de septième de dominante sans fondamentale (voir plus bas), le degré est donc indiqué « “{{Times New Roman|V}}” » (cinq entre guillemets) et non « {{Times New Roman|VII}} » ; * l'accord de quinte augmenté : il est composé d'une tierce majeure et qu'une quinte augmentée. Dans le tableau ci-dessous, * « m » désigne un intervalle mineur ; * « M » un intervalle majeur ou le mode majeur ; * « J » un intervalle juste ; * « d » un intervalle diminué ; * « A » un intervalle augmenté ; * « mh » le mode mineur harmonique ; * « ma » le mode mineur ascendant ; * « md » le mode mineur descendant. {| class="wikitable" |+ Accords de trois notes ! scope="col" rowspan="2" | Nom ! scope="col" rowspan="2" | 3<sup>ce</sup> ! scope="col" rowspan="2" | 5<sup>te</sup> ! scope="col" rowspan="2" | État fondamental ! scope="col" rowspan="2" | 1<sup>er</sup> renversement ! scope="col" rowspan="2" | 2<sup>nd</sup> renversement ! scope="col" colspan="4"| Construit sur les degrés |- ! scope="col" | M ! scope="col" | mh ! scope="col" | ma ! scope="col" | md |- | Accord parfait<br /> majeur || M || J | accord de quinte || accord de sixte || accord de<br />sixte et de quarte | {{Times New Roman|I, IV, V}} || {{Times New Roman|V, VI}} || {{Times New Roman|IV, V}} || {{Times New Roman|III, VI, VII}} |- | Accord parfait<br /> mineur || m || J | accord de quinte || accord de sixte || accord de<br />sixte et de quarte | {{Times New Roman|II, III, VI}} || {{Times New Roman|I, IV}} || {{Times New Roman|I, II}} || {{Times New Roman|I, IV, V}} |- | Accord de<br />quinte diminuée || m || d | accord de<br />quinte diminuée || accord de<br />sixte sensible<br />sans fondamentale || accord de triton<br />sans fondamentale | {{Times New Roman|VII (“V”)}} || {{Times New Roman|II, VII (“V”)}} || {{Times New Roman|VI, VII (“V”)}} || {{Times New Roman|II}} |- | Accord de<br />quinte augmentée || M || A | accord de<br />quinte augmentée || accord de sixte<br />et de tierce sensible || accord de sixte et de quarte<br />sur sensible | &nbsp; || {{Times New Roman|III}} || {{Times New Roman|III}} || &nbsp; |} ==== Accords de quatre notes ==== Les accords de quatre notes sont des accord composés de trois tierces superposées. La dernière note étant le septième degré de la gamme, on parle aussi d'accords de septième. Ces accords sont dissonants : ils contiennent un intervalle de septième (soit une octave montante suivie d'une seconde descendante). Ils laissent donc une impression de « tension ». Il existe sept différents types d'accords, ou « espèces ». Citons l'accord de septième de dominante, l'accord de septième mineure et l'accord de septième majeure. ===== L'accord de septième de dominante ===== [[Fichier:Accord 7e dominante do majeur renversements chiffre.svg|vignette|Accord de septième de dominante de ''do'' majeur et ses renversements, chiffrés.]] L'accord de septième de dominante est l'empilement de trois tierces à partir de la dominante de la gamme, c'est-à-dire du {{Times New Roman|V}}<sup>e</sup> degré. Par exemple, l'accord de septième de dominante de ''do'' majeur est l'accord ''sol''-''si''-''ré''-''fa'', et l'accord de septième de dominante de ''la'' mineur est ''mi''-''sol''♯-''si''-''ré''. L'accord de septième de dominante dont la fondamentale est ''do'' (''do''-''mi''-''sol''-''si''♭) appartient à la gamme de ''fa'' majeur. Que le mode soit majeur ou mineur, il est composé d'une tierce majeure, d'une quinte juste et d'une septième mineure (c'est un accord parfait majeur auquel on ajoute une septième mineure). C'est de loin l'accord de septième le plus utilisé ; il apparaît au {{pc|xvii}}<sup>e</sup> en musique classique. Dans son état fondamental, son chiffrage est {{Times New Roman|V 7/+}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}). Le signe plus indique la sensible. Son premier renversement est appelé « accord de quinte diminuée et sixte » et est noté {{Times New Roman|V 6/<s>5</s>}} (ou {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}). Son deuxième renversement est appelé « accord de sixte sensible », puisque la sixte de l'accord est la sensible de la gamme, et est noté {{Times New Roman|V +6}} (ou {{Times New Roman|V<sup>+6</sup>}}). Son troisième renversement est appelé « accord de quarte sensible » et est noté {{Times New Roman|V +4}} (ou {{Times New Roman|V<sup>+4</sup>}}). [[Fichier:Accord 7e dominante sans fondamentale do majeur renversements chiffre.svg|vignette|Accord de septième de dominante sans fondamentale de ''do'' majeur et ses renversements, chiffrés.]] On utilise aussi l'accord de septième de dominante sans fondamentale ; c'est alors un accord de trois notes. Dans son état fondamental, c'est un « accord de quinte diminuée » placé sur le {{Times New Roman|VII}}<sup>e</sup> degré (mais c'est bien un accord construit sur le {{Times New Roman|V}}<sup>e</sup> degré), noté {{Times New Roman|“V” <s>5</s>}} (ou {{Times New Roman|“V”<sup><s>5</s></sup>}}). Notez les guillemets qui indiquent que la fondamentale V est absente. Dans son premier renversement, c'est un « accord de sixte sensible sans fondamentale » noté {{Times New Roman|“V” +6/3}} (ou {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}}). Dans son second renversement, c'est un « accord de triton sans fondamentale » (puisque le premier intervalle est une quarte augmentée qui comporte trois tons) noté {{Times New Roman|“V” 6/+4}} (ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}). Notons qu'un accord de septième de dominante n'a pas toujours la dominante pour fondamentale : tout accord composé d'une tierce majeure, d'une quinte juste et d'une septième mineure est un accord de septième de dominante et est chiffré {{Times New Roman|<sup>7</sup><sub>+</sub>}}, quel que soit le degré sur lequel il est bâti (certaines notes peuvent avoir une altération accidentelle). ===== Les accords de septième d'espèce ===== Les autres accords de septièmes sont dits « d'espèce ». L'accord de septième mineure est l'accord de septième formé sur la fondamentale d'une gamme mineure ''naturelle''. Par exemple, l'accord de septième mineure de ''la'' est ''la''-''do''-''mi''-''sol''. Il est composé d'une tierce mineure, d'une quinte juste et d'une septième mineure (c'est un accord parfait mineur auquel on ajoute une septième mineure). L'accord de septième majeure est l'accord de septième formé sur la fondamentale d'une gamme majeure. Par exemple, L'accord de septième majeure de ''do'' est ''do''-''mi''-''sol''-''si''. Il est composé d'une tierce majeure, d'une quinte juste et d'une septième majeure (c'est un accord parfait majeur auquel on ajoute une septième majeure). ==== Utilisation du chiffrage ==== Le chiffrage est utilisé de deux manières. La première manière, c'est la notation de la basse continue. La basse continue est une technique d'improvisation utilisée dans le baroque pour l'accompagnement d'instruments solistes. Sur la partition, on indique en général la note de basse de l'accord et le chiffrage en chiffres arabes. La seconde manière, c'est pour l'analyse d'une partition. Le fait de chiffrer les accords permet de mieux en comprendre la structure. De manière générale, on peut retenir que : * le chiffrage « 5 » indique un accord parfait, superposition d'une tierce (majeure ou mineure) et d'une quinte juste ; * le chiffrage « 6 » indique le premier renversement d'un accord parfait ; * le chiffrage « 6/4 » indique le second renversement d'un accord parfait ; * chiffrage « 7/+ » indique un accord de septième de dominante ; * le signe « + » indique en général que la note de l'intervalle est la sensible ; * un intervalle barré désigne un intervalle diminué. [[fichier:Accords gamme do majeur la mineur.svg|class=transparent| center | Principaux accords construits sur les gammes de ''do'' majeur et de ''la'' mineur harmonique.]] === Notation « jazz » === En jazz et de manière générale en musique rock et populaire, la base d'un accord est la triade composée d'une tierce (majeure ou mineure) et d'une quinte juste. Pour désigner un accord, on utilise la note fondamentale, éventuellement désigné par une lettre dans le système anglo-saxon (A pour ''la'' etc.), suivi d'une qualité (comme « m », « + »…). Les renversements ne sont pas notés de manière particulière, ils sont notés comme les formes fondamentales. Dans les deux tableaux suivants, la fondamentale est notée X (remplace le C pour un accord de ''do'', le D pour un accord de ''ré''…). La construction des accords est décrite par la suite. [[Fichier:Arbre accords triades 5d5J5A.svg|vignette|upright=1.5|Formation des triades présentée sous forme d'arbre.]] {| class="wikitable" |+ Notation des principales triades |- | ! scope="col" | Tierce<br />mineure (3m) ! scope="col" | Tierce<br />majeure (3M) |- ! scope="row" | Quinte diminuée (5d) | X<sup>o</sup>, Xm<sup>♭5</sup>, X–<sup>♭5</sup> || |- ! scope="row" | Quinte juste (5J) | Xm, X– || X |- ! scope="row" | Quinte augmentée (5A) | || X+, X<sup>♯5</sup> |} [[Fichier:Triades do.svg|class=transparent|center|Triades de do.]] {| class="wikitable" |+ Notation des principaux accords de septième |- | colspan="2" | ! scope="col" | Tierce<br />mineure (3m) ! scope="col" | Tierce<br />majeure (3M) |- ! scope="row" rowspan="2" | Quinte<br />diminuée (5d) ! scope="row" | Septième diminuée (7d) | X<sup>o7</sup> || |- ! scope="row" | Septième mineure (7m) | Xm<sup>7(♭5)</sup>, X–<sup>7(♭5)</sup>, X<sup>Ø</sup> || |- ! scope="row" rowspan="3" | Quinte<br />juste (5J) ! scope="row" | Sixte majeure (6M) | Xm<sup>6</sup> || X<sup>6</sup> |- ! scope="row" | Septième mineure (7m) | Xm<sup>7</sup>, X–<sup>7</sup> || X<sup>7</sup> |- ! scope="row" | Septième majeure (7M) | Xm<sup>maj7</sup>, X–<sup>maj7</sup>, Xm<sup>Δ</sup>, X–<sup>Δ</sup> || X<sup>maj7</sup>, X<sup>Δ</sup> |- ! scope="row" rowspan="2" | Quinte<br />augmentée (5A) ! scope="row" | Septième mineure (7m) | || X+<sup>7</sup> |- ! scope="row" | Septième majeure (7M) | || X+<sup>maj7</sup> |} [[Fichier:Arbre accords septieme.svg|class=transparent|center|Formation des accords de septième présentée sous forme d'arbre.]] &nbsp; &nbsp; [[Fichier:Accords do septieme.svg|class=transparent|center|Accord de do septième.]] On notera que l'intervalle de sixte majeure est l'enharmonique de celui de septième diminuée (6M = 7d). [[File:Principaux accords do.svg|class=transparent|center|Principaux accords de do.]] ==== Triades ==== ; Accords fondés sur une tierce majeure * accord parfait majeur : pas de notation *: p. ex. « ''do'' » ou « C » pour l'accord parfait de ''do'' majeur (''do'' - ''mi'' - ''sol'') ; Accords fondés sur une tierce mineure * accord parfait mineur : « m », « min » ou « – » *: « ''do'' m », « ''do'' – », « Cm », « C– »… pour l'accord parfait de ''do'' mineur (''do'' - ''mi''♭ - ''sol'') ==== Triades modifiées ==== ; Accords fondés sur une tierce majeure * accord augmenté (la quinte est augmentée) : aug, +, ♯5 *: « ''do'' aug », « ''do'' + », « ''do''<sup>♯5</sup> » « Caug », « C+ » ou « C<sup>♯5</sup> » pour l'accord de ''do'' augmenté (''do'' - ''mi'' - ''sol''♯) : L'accord augmenté est un empilement de tierces majeures. Ainsi, un accord augmenté a deux notes communes avec deux autres accords augmentés : C+ (''do'' - ''mi'' - ''sol''♯) a deux notes communes avec A♭+ (''la''♭ - ''do'' - ''mi'') et avec E+ (''mi'' - ''sol''♯ - ''si''♯) ; et on remarque que ces trois accords sont en fait enharmoniques (avec les enharmonies ''la''♭ = ''sol''♯ et ''si''♯ = ''do''). En effet, l'octave comporte six tons (sous la forme de cinq tons et deux demi-tons), et une tierce majeure comporte deux tons, on arrive donc à l'octave en ajoutant une tierce majeure à la dernière note de l'accord. ; Accords fondés sur une tierce mineure * accord diminué (la quinte est diminuée) : dim, o, ♭5 *: « ''do'' dim », « ''do''<sup>o</sup> », « ''do''<sup>♭5</sup> », « Cdim », « C<sup>o</sup> » ou « C<sup>♭5</sup> » pour l'accord de ''do'' diminuné (''do'' - ''mi''♭ - ''sol''♭) : On remarque que la quinte diminuée est l'enharmonique de la quarte augmentée et est l'intervalle appelé « triton » (car composé de trois tons). ; Accords fondés sur une tierce majeure ou mineure * accord suspendu de seconde : la tierce est remplacée par une seconde majeure : sus2 *: « ''do''<sup>sus2</sup> » ou « C<sup>sus2</sup> » pour l'accord de ''do'' majeur suspendu de seconde (''do''-''ré''-''sol'') * accord suspendu de quarte : la tierce est remplacée par une quarte juste : sus4 *: « ''do''<sup>sus4</sup> » ou « C<sup>sus4</sup> » pour l'accord de ''do'' majeur suspendu de quarte (''do''-''fa''-''sol'') ==== Triades appauvries ==== ; Accords fondés sur une tierce majeure ou mineure * accord de puissance : la tierce est omise, l'accord n'est constitué que de la fondamentale et de la quinte juste : 5 *: « ''do''<sup>5</sup> », « C<sup>5</sup> » pour l'accord de puissance de ''do'' (''do'' - ''la'') {{note|Très utilisé dans les musiques rock, hard rock et heavy metal, il est souvent joué renversé (''la'' - ''do'') ou bien avec l'ajout de l'octave (''do'' - ''la'' - ''do'').}} ==== Triades enrichies ==== ; Accords fondés sur une tierce majeure * accord de septième (la 7<sup>e</sup> est mineure) : 7 *: « ''do''<sup>7</sup> », « C<sup>7</sup> » pour l'accord de ''do'' septième, appelé « accord de septième de dominante de ''fa'' majeur » en musique classique (''do'' - ''mi'' - ''sol'' - ''si''♭) * accord de septième majeure : Δ, 7M ou maj7 *: « ''do'' <sup>Δ</sup> », « ''do'' <sup>maj7</sup> », « C<sup>Δ</sup> », « C<sup>7M</sup> »… pour l'accord de ''do'' septième majeure (''do'' - ''mi'' - ''sol'' - ''si'') ; Accords fondés sur une tierce mineure * accord de mineur septième (la tierce et la 7<sup>e</sup> sont mineures) : m7, min7 ou –7 *: « ''do'' m<sup>7</sup> », « ''do'' –<sup>7</sup> », « Cm<sup>7</sup> », « C–<sup>7</sup> »… pour l'accord de ''do'' mineur septième, appelé « accord de septième de dominante de ''fa'' mineur » en musique classique (''do'' - ''mi''♭ - ''sol'' - ''si''♭) * accord mineure septième majeure : m7M, m7maj, mΔ, –7M, –7maj, –Δ *: « ''do'' m<sup>7M</sup> », « ''do'' m<sup>maj7</sup> », « ''do'' –<sup>Δ</sup> », « Cm<sup>7M</sup> », « Cm<sup>maj7</sup> », « C–<sup>Δ</sup> »… pour l'accord de ''do'' mineur septième majeure (''do'' - ''mi''♭ - ''sol'' - ''si'') * accord de septième diminué (la quinte et la septième sont diminuée) : dim 7 ou o7 *: « ''do'' dim<sup>7</sup> », « ''do''<sup>o7</sup> », « Cdim<sup>7</sup> » ou « C<sup>o7</sup> » pour l'accord de ''do'' septième diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si''♭) * accord demi-diminué (seule la quinte est diminuée, la septième est mineure) : Ø ou –7(♭5) *: « ''do''<sup>Ø</sup> », « ''do''<sup>7(♭5)</sup> », « C<sup>Ø</sup> » ou « C<sup>7♭5</sup> » pour l'accord de ''do'' demi-diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si'') === Construction pythagoricienne des accords === Nous avons vu au débuts que lorsque l'on joue deux notes en même temps, leurs vibrations se superposent. Certaines superpositions créent un phénomène de battement désagréable, c'est le cas des secondes. Dans le cas d'une tierce majeure, les fréquences des notes quadruple et quintuple d'une même base : les fréquences s'écrivent 4׃<sub>0</sub> et 5׃<sub>0</sub>. Cette superposition de vibrations est agréable à l'oreille. Nous avons également vu que dans le cas d'une quinte juste, les fréquences sont le double et le triple d'une même base, ou encore le quadruple et sextuple si l'on considère la moitié de cette base. Ainsi, dans un accord parfait majeur, les fréquences des fondamentales des notes sont dans un rapport 4, 5, 6. De même, dans le cas d'un accord parfait mineur, les proportions sont de 1/6, 1/5 et 1/4. {{voir|[[../Caractéristiques_et_notation_des_sons_musicaux#Construction_pythagoricienne_et_gamme_de_sept_tons|Caractéristiques et notation des sons musicaux &gt; Construction pythagoricienne et gamme de sept tons]]}} === Un peu de physique : interférences === Les sons sont des vibrations. Lorsque l'on émet deux sons ou plus simultanément, les vibrations se superposent, on parle en physique « d'interférences ». Le modèle le plus simple pour décrire une vibration est la [[w:fr:Fonction sinus|fonction sinus]] : la pression de l'air P varie en fonction du temps ''t'' (en secondes, s), et l'on a pour un son « pur » : : P(''t'') ≈ sin(2π⋅ƒ⋅''t'') où ƒ est la fréquence (en hertz, Hz) du son. Si l'on émet deux sons de fréquence respective ƒ<sub>1</sub> et ƒ<sub>2</sub>, alors la pression vaut : : P(''t'') ≈ sin(2π⋅ƒ<sub>1</sub>⋅''t'') + sin(2π⋅ƒ<sub>2</sub>⋅''t''). Nous avons ici une [[w:fr:Identité trigonométrique#Transformation_de_sommes_en_produits,_ou_antilinéarisation|identité trigonométrique]] dite « antilinéarisation » : : <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{f_1 + f_2}{2}t \right ) \cdot \sin \left ( 2\pi \frac{f_1 - f_2}{2}t \right ).</math> On peut étudier simplement deux situations simples. [[Fichier:Battements interferentiels.png|vignette|Deux sons de fréquences proches créent des battements : la superposition d'une fréquence et d'une enveloppe.]] La première, c'est quand les fréquences ƒ<sub>1</sub> et ƒ<sub>2</sub> sont très proches. Alors, la moyenne (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2 est très proche de ƒ<sub>1</sub> et ƒ<sub>2</sub> ; et la demie différence (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2 est très proche de zéro. On a donc une enveloppe de fréquence très faible, (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2, dans laquelle s'inscrit un son de fréquence moyenne, (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2. C'est cette enveloppe de fréquence très faible qui crée les battements, désagréables à l'oreille. Sur l'image ci-contre, le premier trait rouge montre un instant où les vibrations sont opposées ; elles s'annulent, le son s'éteint. Le second trait rouge montre un instant où les vibrations sont en phase : elle s'ajoutent, le son est au plus fort. {{clear}} La seconde, c'est lorsque les deux fréquences sont des multiples entiers d'une même fréquence fondamentale ƒ<sub>0</sub> : ƒ<sub>1</sub> = ''n''<sub>1</sub>⋅ƒ<sub>0</sub> et ƒ<sub>0</sub> = ''n''<sub>0</sub>⋅ƒ<sub>0</sub>. On a alors : : <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{n_1 + n_2}{2}f_0 \cdot t \right ) \cdot \sin \left ( 2\pi \frac{n_1 - n_2}{2}f_0 \cdot t \right ).</math> On multiplie donc deux fonctions qui ont des fréquences multiples de ƒ<sub>0</sub>. La différence minimale entre ''n''<sub>1</sub> et ''n''<sub>2</sub> vaut 1 ; on a donc une enveloppe dont la fréquence est au minimum la moitié de ƒ<sub>0</sub>, c'est-à-dire un son une octave en dessous de ƒ<sub>0</sub>. Donc, cette enveloppe ne crée pas d'effet de battement, ou plutôt, le battement est trop rapide pour être perçu comme tel. Dans cette enveloppe, on a une fonction sinus dont la fréquence est également un multiple de ƒ<sub>0</sub> ; l'enveloppe et la fonction qui y est inscrite ont donc de nombreux « points communs », d'où l'effet harmonieux. === Le tonnetz === [[File:Speculum musicae.png|thumb|right|225px|Euler, ''De harmoniæ veris principiis'', 1774, p. 350.]] En allemand, le terme ''Tonnetz'' (se prononce « tône-netz ») signifie « réseau tonal ». C'est une représentation graphique des notes qui a été imaginée par [[w:Leonhard Euler|Leonhard Euler]] en 1739. Cette représentation graphique peut aider à la mémorisation de certains concepts de l'harmonie. Cependant, son application est très limitée : elle ne concerne que l'intonation juste d'une part, et que les accords parfait des tonalités majeures et mineures naturelles d'autre part. La représentation contenant les douze notes de la musique savante occidentale, on peut bien sûr représenter d'autres objets, comme les accords de septième ou les accords diminués, mais la représentation graphique est alors compliquée et perd son intérêt pédagogique. On part d'une note, par exemple le ''do''. Si on progresse vers la droite, on monte d'une quinte juste, donc ''sol'' ; vers la gauche, on descend d'une quinte juste, donc ''fa''. Si on va vers le bas, on monte d'une tierce majeure, donc ''mi'' ; si on va vers le haut, on descend d'une tierce majeure, donc ''la''♭ ou ''sol''♯ fa — do — sol — ré | | | | la — mi — si — fa♯ | | | | do♯ — sol♯ — ré♯ — si♭ La figure forme donc un filet, un réseau. On voit que ce réseau « boucle » : si on descend depuis le ''do''♯, on monte d'une tierce majeure, on obtient un ''mi''♯ qui est l'enharmonique du ''fa'' qui est en haut de la colonne. Si on va vers la droite à partir du ''ré'', on obtient le ''la'' qui est au début de la ligne suivante. Si on ajoute des diagonales allant vers la droite et le haut « / », on met en évidence des tierces mineures : ''la'' - ''do'', ''mi'' - ''sol'', ''si'' - ''ré'', ''do''♯ - ''mi''… fa — do — sol — ré | / | / | / | la — mi — si — fa♯ | / | / | / | do♯ — sol♯ — ré♯ — si♭ Donc les liens représentent : * | : tierce majeure ; * — : quinte juste ; * / : tierce mineure. [[Fichier:Tonnetz carre accords fr.svg|thumb|Tonnetz avec les accords parfaits. Les notes sont en notation italienne et les accords en notation jazz.]] On met ainsi en évidence des triangles dont un côté est une quinte juste, un côté une tierce majeure et un côté une tierce mineure ; c'est-à-dire que les notes aux sommets du triangle forment un accord parfait majeur (par exemple ''do'' - ''mi'' - ''sol'') : <div style="font-family:courier; background-color:#fafafa"> fa&nbsp;&nbsp;—&nbsp;'''do&nbsp;&nbsp;&nbsp;—&nbsp;sol'''&nbsp;—&nbsp;ré<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;'''|&nbsp;&nbsp;&nbsp;/'''&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;|<br /> la&nbsp;&nbsp;—&nbsp;'''mi'''&nbsp;&nbsp;&nbsp;—&nbsp;si&nbsp;&nbsp;—&nbsp;fa♯<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;/&nbsp;&nbsp;|<br /> do♯&nbsp;—&nbsp;sol♯&nbsp;—&nbsp;ré♯&nbsp;—&nbsp;si♭ </div> ou un accord parfait mineur (''la'' - ''do'' - ''mi''). <div style="font-family:courier; background-color:#fafafa"> fa&nbsp;&nbsp;—&nbsp;'''do'''&nbsp;&nbsp;&nbsp;—&nbsp;sol&nbsp;—&nbsp;ré<br /> |&nbsp;&nbsp;'''/&nbsp;&nbsp;|'''&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;|<br /> '''la&nbsp;&nbsp;—&nbsp;mi'''&nbsp;&nbsp;&nbsp;—&nbsp;si&nbsp;&nbsp;—&nbsp;fa♯<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;/&nbsp;&nbsp;|<br /> do♯&nbsp;—&nbsp;sol♯&nbsp;—&nbsp;ré♯&nbsp;—&nbsp;si♭ </div> Un triangle représente donc un accord, et un sommet représente une note. Si on passe d'un triangle à un triangle voisin, alors on passe d'un accord à un autre accord, les deux accords ayant deux notes en commun. Ceci illustre la notion de « plus court chemin » en harmonie : si on passe d'un accord à un autre en gardant un côté commun, alors on a un mouvement conjoint sur une seule des trois voix. Par rapport à l'harmonie fonctionnelle : les accords sont contigus à leur fonction, par exemple en ''do'' majeur : * fonction de tonique ({{Times New Roman|I}}) : C, A– et E– sont contigus ; * fonction de sous-dominante ({{Times New Roman|IV}}) : F et D– sont contigus ; * fonction de dominante ({{Times New Roman|V}}) : G et B<sup>o</sup> sont contigus. On notera que les triangles d'un schéma ''tonnetz'' ne représentent que des accords parfaits. Pour représenter un accord de quinte diminuée (''si'' - ''ré'' - ''fa'') ou les accords de septième, en particulier l'accord de septième de dominante, il faut étendre le ''tonnetz'' et l'on obtient des figures différentes. Par ailleurs, il est adapté à ce que l'on appelle « l'intonation juste », puisque tous les intervalles sont idéaux. [[Fichier:Tonnetz carre accords etendu fr.svg|vignette|Tonnetz étendu.]] [[Fichier:Tonnetz carre do majeur accords fr.svg|vignette|Tonnetz de la tonalité de ''do'' majeur. La représentation de l'accord de quinte diminuée sur ''si'' (B<sup>o</sup>) est une ligne et non un triangle.]] [[Fichier:Tonnetz carre do mineur accords fr.svg|vignette|Tonnetz des tonalités de ''do'' mineur naturel (haut) et ''do'' mineur harmonique (bas).]] Si l'on étend un peu le réseau : ré♭ — la♭ — mi♭ — si♭ — fa | / | / | / | / | fa — do — sol — ré — la | / | / | / | / | la — mi — si — fa♯ — do♯ | / | / | / | / | do♯ — sol♯ — ré♯ — la♯ — mi♯ | / | / | / | / | mi♯ — do — sol — ré — la on peut donc trouver des chemins permettant de représenter les accords de septième de dominante (par exemple en ''do'' majeur, G<sup>7</sup>) fa / sol — ré | / si et des accords de quinte diminuée (en ''do'' majeur : B<sup>o</sup>) fa / ré / si Une gamme majeure ou mineure naturelle peut se représenter par un trapèze rectangle : ''do'' majeur fa — do — sol — ré | / la — mi — si et ''do'' mineur la♭ — mi♭ — si♭ / | fa — do — sol — ré En revanche, la représentation d'une tonalité nécessite d'étendre le réseau afin de pouvoir faire figurer tous les accords, deux notes sont représentées deux fois. La représentation des tonalités mineures harmoniques prend une forme biscornue, ce qui nuit à l'intérêt pédagogique de la représentation. [[Fichier:Neo-Riemannian Tonnetz.svg|vignette|upright=2|Tonnetz avec des triangles équilatéraux.]] On peut réorganiser le schéma en décalant les lignes, afin d'avoir des triangles équilatéraux. Sur la figure ci-contre (en notation anglo-saxonne) : * si on monte en allant vers la droite « / », on a une tierce mineure ; * si on descend en allant vers la droite « \ », on a une tierce majeure ; * les liens horizontaux « — » représentent toujours des quintes justes * les triangles pointe en haut sont des accords parfaits mineurs ; * les triangles pointe en bas sont des accords parfaits majeurs. On a alors les accords de septième de dominante F / G — D \ / B et de quinte diminuée F / D / B les tonalités majeures F — C — G — D \ / A — E — B et les tonalités mineures naturelles A♭ — E♭ — B♭ / \ F — C — G — D == Notes et références == {{références}} == Voir aussi == === Bibliographie === * {{ouvrage |prénom1=Francesco |nom1=Gasparini |titre=L'Armonico Pratico al Cimbalo |éditeur=Giusseppe Antonio Silvani |lieu=Bologne |année=1772 |lang=it |url=https://imslp.org/wiki/L'Armonico_Pratico_al_Cimbalo_(Gasparini%2C_Francesco)}} === Liens externes === {{wikipédia|Consonance (harmonie tonale)}} {{wikipédia|Disposition de l'accord}} {{wikisource|Petit Manuel d’harmonie}} * {{lien web | url = https://www.apprendrelesolfege.com/chiffrage-d-accords | titre = Chiffrage d'accords (classique) | site = Apprendrelesolfege.com | consulté le = 2020-12-03 }} * {{lien web | url = https://www.coursd-harmonie.fr/introduction/introduction2_le_chiffrage_d_accords.php | titre = Introduction II : Le chiffrage d'accords | site = Cours d'harmonie.fr | consulté le = 2021-12-14 }} * {{lien web | url=https://www.coursd-harmonie.fr/ | titre = Cours d'harmonie en ligne | auteur = Jean-Baptiste Voinet | site=coursd-harmonie.fr | consulté le = 2021-12-20 }} * {{lien web | url=http://e-harmonie.e-monsite.com/ | titre = Cours d'harmonie classique en ligne | auteur = Olivier Miquel | site=e-harmonie | consulté le = 2021-12-24 }} * {{lien web | url=https://fr.audiofanzine.com/theorie-musicale/editorial/dossiers/les-gammes-et-les-modes.html | titre = Les bases de l’harmonie | site = AudioFanzine | date = 2013-07-23 | consulté le = 2024-01-12 }} * {{lien web | url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/l-accord-de-tristan-dechiffre-et-desacralise-2481605 | titre = L’Accord de Tristan déchiffré et désacralisé ! | site = France Musique | date = 2025-10-22 | consulté le = 2025-10-23 }} * {{lien web | url=https://www.youtube.com/watch?v=X62SAnyfQYw | titre = Qu'est-ce que la basse continue dans la musique baroque ? | auteur = Clément Rochefort | site = YouTube/France Musique | date = 2022-05-02 | consulté le = 2026-08-04 }} ---- ''[[../Mélodie|Mélodie]]'' &lt; [[../|↑]] &gt; ''[[../Représentation musicale|Représentation musicale]]'' [[Catégorie:Formation musicale (livre)|Harmonie]] 35j1v71jzwbuswy1d5wxzx8761okxty 773906 773905 2026-10-02T08:20:55Z Cdang 1202 /* Consonance et dissonance */ précision 773906 wikitext text/x-wiki {{Bases de solfège}} <span style="font-size:25px;">6. Harmonie</span> L'harmonie désigne les notes jouées en même temps, soit plusieurs instruments jouant chacun une note, soit un instrument jouant un accord (instrument dit polyphonique). == Première approche == L'exemple le plus simple d'harmonie est sans doute la chanson en canon : c'est un chant polyphonique, c'est-à-dire à plusieurs voix, chaque voix chantant la même chose en décalé. Prenons par exemple ''Vent frais, vent du matin'' (la version originale est ''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609) : [[Fichier:Vent frais vent du matin.svg|class=transparent|center|Partition de ''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]] [[Fichier:Vent frais vent du matin.midi|vignette|''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]] nous voyons que les voix se superposent de manière « harmonieuse ». Les notes de chaque voix se correspondent point par point (avec un retard), c'est donc un type d'harmonie polyphonique appelé « contrepoint ». Considérons la première note de la mesure 6 pour chaque voix. Nous avons la superposition des notes ''ré''-''fa''-''la'' (du grave vers l'aigu) ; la superposition de notes jouées ou chantées ensembles s'appelle un accord. Cet accord ''ré''-''fa''-''la'' porte le nom « d'accord parfait de ''ré'' mineur » : * « ''ré'' » car la note fondamentale est un ''ré'' ; * « parfait » car il est l'association d'une tierce, ''ré''-''fa'', et d'une quinte juste, ''ré''-''la'' ; * « mineur » car le premier intervalle, ''ré''-''fa'', est une tierce mineure. Considérons maintenant un chant accompagné au piano. Le piano peut jouer plusieurs notes en même temps, il peut jouer des accords. [[Fichier:Au clair de le lune chant et piano.svg|class=transparent|center|Deux premières mesure d’Au clair de la lune.]] [[Fichier:Au clair de le lune chant et piano.midi|vignette|Deux premières mesure d’Au clair de la lune.]] L'accord, les notes à jouer simultanément, sont écrites « en colonne ». Lorsqu'on les énonce, on les lit de bas en haut mais le pianiste les joue en pressant les touches du clavier en même temps, de manière « plaquée ». Le premier accord est composé des notes ''do''-''mi''-''sol'' ; il est appelé « accord parfait de ''do'' majeur » car la note fondamentale est ''do'', qu'il est l'association d'une tierce et d'une quinte juste et que le premier intervalle, ''do''-''mi'', est une tierce majeure. Si l'on lit toutes les notes, de la plus grave à la plus aiguë, nous avons : ''do'' (à la clef de ''fa'', main gauche du piano) - ''do'' - ''mi'' - ''sol'' (main droite du piano) - ''do'' (chant) ; nous voyons que la note ''do'' est jouée plusieurs fois, cela ne change pas la qualité de l'accord. == Consonance et dissonance == [[Fichier:Seconde majeure septieme mineure.svg|vignette|Seconde majeure et son renversement, le septième mineure.]] Les notions de consonance et de dissonance sont culturelles et changent selon l'époque. Nous pouvons néanmoins noter que : * l'accord de seconde, et son renversement la septième, créent des battements, les notes « frottent », c'est un intervalle harmonique dissonant ; mais dans le cas de la septième, comme les notes sont éloignées, le frottement est moins perceptible ; * les accords d'unisson, de tierce, quarte et quinte sonnent agréablement à l'oreille, ils sont consonants. Dans la musique savante européenne, au début du Moyen-Âge, seuls les accords d'unisson, de quarte et de quinte étaient considérés comme consonants, d'où leur qualification de « juste ». La tierce, et son renversement la sixte, étaient perçues comme dissonantes ; à partir de la Renaissance, ces deux intervalles sont considérés comme consonants. L'harmonie joue avec les consonances et les dissonances. Dans un premier temps, les harmonies dissonantes sont utilisées pour créer des tensions qui sont ensuite résolues, on utilise des successions « consonant-dissonant-consonant ». À force d'entendre des intervalles considérés comme dissonants, l'oreille s'habitue et certains finissent par être considérés comme consonants ; c'est ce qui est arrivé à la tierce et à la sixte à la fin du Moyen Âge avec le contrepoint, puis à la septième plus tardivement. Il faut ici aborder la notion d'harmonique des notes. [[File:Harmoniques de do.svg|thumb|Les six premières harmoniques de ''do''.]] Lorsque l'on joue une note, on entend d'autres notes plus aigües et plus faibles ; la note jouée est appelée la « fondamentale » et les notes plus aigües et plus faibles sont les « harmoniques ». C'est cette accumulation d'harmoniques qui donne la couleur au son, son timbre, qui fait qu'un piano ne sonne pas comme un violon. Par exemple, si l'on joue un ''do''<sup>1</sup><ref>Pour la notation des octaves, voir ''[[../Représentation_musicale#Désignation_des_octaves|Représentation musicale &gt; Désignation des octaves]]''.</ref> (fondamentale), on entend le ''do''<sup>2</sup> (une octave plus aigu), puis un ''sol''<sup>2</sup>, puis encore un ''do''<sup>3</sup> plus aigu, puis un ''mi''<sup>3</sup>, puis encore un ''sol''<sup>3</sup>, puis un ''si''♭<sup>3</sup>… Ainsi, puisque lorsque l'on joue un ''do'' on entend aussi un ''sol'' très léger, alors jouer un ''do'' et un ''sol'' simultanément n'est pas choquant. De même pour ''do'' et ''mi''. De là vient la notion de consonance. Le statut du ''si''♭ est plus ambigu. Il fait partie des harmoniques qui sonnent naturellement, mais il forme une seconde descendante avec le ''do'', intervalle dissonant. Par ailleurs, on remarque que le ''si''♭ ne fait pas partie de la gamme de ''do'' majeur, contrairement au ''sol'' et au ''mi''. Pour le jeu sur les dissonances, on peut écouter par exemple la ''Toccata'' en ''ré'' mineur, op. 11 de Sergueï Prokofiev (1912). : {{lien web |url=https://www.youtube.com/watch?v=AVpnr8dI_50 |titre=Yuja Wang Prokofiev Toccata |site=YouTube |date=2019-02-26 |consulté le=2021-12-19}} == Contrepoint == Dans le chant grégorien, la notion d'accord n'existe pas. L'harmonie provient de la superposition de plusieurs mélodies, notamment dans ce que l'on appelle le « contrepoint ». Le terme provient du latin ''« punctum contra punctum »'', littéralement « point par point », et désigne le fait que les notes de chaque voix se correspondent. L'exemple le plus connu de contrepoint est le canon, comme ''Frère Jacques'' ou bien ''Vent frais, vent du matin'' présenté ci-dessus : chaque note d'un couplet correspond à une note du couplet précédent. Certains morceaux sont bâtis sur une écriture « en miroir » : l'ordre des notes est inversé entre les deux voix, ou bien les intervalles sont inversés (« mouvement contraire » : une tierce montante sur une voix correspond à une tierce descendante sur l'autre). On peut également citer le « mouvement oblique » (une des voix, le bourdon, chante toujours la même note) et le mouvement parallèle (les deux voix chantent le même air mais transposé, l'une est plus aiguë que l'autre). Nous reproduisons ci-dessous le début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel. [[Fichier:Haendel Sonate en trio re mineur debut canon.svg | vignette | center | upright=2 | Début du second ''Allergo'' de la sonate en trio en ''ré'' mineur de Haendel.]] [[Fichier:Haendel Sonate en trio re mineur debut.midi | vignette | Début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.]] Nous avons mis en évidence la construction en canon avec des encadrés de couleur : sur les quatre premières mesures, nous voyons trois thèmes repris alternativement par une voix et par l'autre. Ce type de procédé est très courant dans la musique baroque. Les procédés du contrepoint s'appliquent également à la danse : * unisson : les danseurs et danseuses font les mêmes gestes en même temps ; * répétition : le fait de répéter une série de gestes, une « phrase dansante » ; * canon : les gestes sont faits avec un décalage régulier d'un danseur ou d'une danseuse à l'autre ; * cascade : forme de canon dans laquelle le décalage est très petit ; * contraste : deux danseur·euses, ou deux groupes, ont des gestuelles très différentes ; * accumulation : la gestuelle se complexifie par l'ajout d'éléments au fur et à mesure ; ou bien le nombre de danseur·euses augmente ; * dialogue : les gestes de danseur·euses ou de groupes se répondent ; * contre-point : la gestuelle d'un ou une danseuse se superpose à la gestuelle d'un groupe ; * lâcher-rattraper : les danseurs et danseuses alternent danse à l'unisson et gestuelles indépendantes. : {{lien web | url=https://www.youtube.com/watch?v=wgblAOzedFc | titre=Les procédés de composition en danse | auteur= Doisneau Sport TV | site=YouTube | date=2020-03-16 | consulté le=2021-01-21 }} {{...}} == Ostinato == L'ostinato, mot italien signifiant « obstiné », c'est le fait de jouer en boucle un passage. L'ostinato peut ainsi former une structure autour de laquelle peut se développer le morceau. Lorsque l'ostinato est joué par l'instrument de basse (basson, violoncelle, viole de gambe basse, flûte à bec basse, main gauche du piano…), on parle de basse obstinée. Les grilles d'accord en jazz sont des ostinatos (ou ostinati) joués par la section rythmique. Exemples : * {{lien web |url=https://www.youtube.com/watch?v=QZpGe5rNJkI&t=41s |titre=Epic Patty Cake Song (I'll Think Of You) |auteur= Kurt Hugo Schneider |site=YouTube |date=2014-08-12 |consulté le=2026-01-24}} : un ostinato de percussions corporelles ; * {{lien web |url=https://www.youtube.com/watch?v=Kv-37-nXC48 |titre=Pow Wow ''Le lion est mort ce soir'' Les Victoires de la Musique 1993 |auteur=Solomon Linda |date=2015-02-02 |consulté le=2026-01-24}} : dans ce chant ''a capella'' (sans accompagnement d'instrument), les voix de basse forment une basse obstinée, sur les syllabes « oëap » durant le couplet et « awimbowé » sur le refrain ; * {{lien web |url=https://www.youtube.com/watch?v=JvNQLJ1_HQ0 |titre=Pachelbel Canon in D Major - the original and best version. |auteur=Voices of Music |site=YouTube |date=2008-09-02 |consulté le=2026-01-24}} : le ''Canon de Pachelbel de ''ré'' mineur'' (''Canon per tre Violini e Basso'', Johann Pachelbel, vers 1680) est construit sur une basse obstinée de huit mesures. == Les accords en général == Initialement, on a des chants polyphoniques, des voix qui chantent chacune une mélodie, les mélodies se mêlant. On remarque que certaines superpositions de notes sonnent de manière plus ou moins agréables, consonantes ou dissonantes. On en vient alors à associer ces notes, c'est-à-dire à considérer dès le départ la superposition de ces notes et non pas la rencontre de ces notes au gré des mélodies. Ces groupes de notes superposées forment les accords. En Europe, cette notion apparaît vers le {{pc|xiv}}<sup>e</sup> siècle avec notamment la ''[[wikipedia:fr:Messe de Notre Dame|Messe de Notre Dame]]'' de Guillaume de Machaut (vers 1360-1365). La notion « d'accord parfait » est consacrée par [[wikipedia:fr:Jean-Philippe Rameau|Jean-Philippe Rameau]] dans son ''Traité de l'harmonie réduite à ses principes naturels'', publié en 1722. === Qu'est-ce qu'un accord ? === Un accord est un ensemble d'au minimum trois notes jouées en même temps. « Jouées » signifie qu'il faut qu'à un moment donné, elles sonnent en même temps, mais le début ou la fin des notes peut être à des instants différents. Considérons que l'on joue les notes ''do'', ''mi'' et ''sol'' en même temps. Cet accord s'appelle « accord de ''do'' majeur ». En musique classique, on lui adjoint l'adjectif « parfait » : « accord parfait de ''do'' majeur ». Nous représentons ci-dessous trois manière de faire l'accord : avec trois instruments jouant chacun une note : [[Fichier:Do majeur trois portees.svg|class=transparent|center|Accord de ''do'' majeur avec trois instruments différents.]] Avec un seul instrument jouant simultanément les trois notes : [[Fichier:Chord C.svg|class=transparent|center|Accord de ''do'' majeur joué par un seul instrument.]] L'accord tel qu'il est joué habituellement par une guitare d'accompagnement : [[Fichier:Do majeur guitare.svg|class=transparent|center|Accord de ''do'' majeur à la guitare.]] Pour ce dernier, nous représentons le diagramme indiquant la position des doigts sur le manche au dessus de la portée et la tablature en dessous. Ici, c'est au total six notes qui sont jouées : ''mi'' grave, ''do'' médium, ''mi'' médium, ''sol'' médium, ''do'' aigu, ''mi'' aigu. Mais il s'agit bien des trois notes ''do'', ''mi'' et ''sol'' jouées à des octaves différentes. Nous remarquons également que la note de basse (la note la plus grave), ''mi'', est différente de la note fondamentale (celle qui donne le nom à l'accord), ''do'' ; l'accord est dit « renversé » (voir plus loin). === Comment joue-t-on un accord ? === Les notes ne sont pas forcément jouées en même temps ; elles peuvent être « égrainées », jouée successivement, ce que l'on appelle un arpège. La partition ci-dessous montre six manières différentes de jouer un accord de ''la'' mineur à la guitare, plaqué puis arpégé. [[Fichier:La mineur differentes executions.svg|class=transparent|center|Différentes exécution de l'accord de do majeur à la guitare.]] [[Fichier:La mineur differentes executions midi.midi|vignette|Différentes exécution de l'accord de la mineur à la guitare.]] Vous pouvez écouter l'exécution de cette partition avec le lecteur ci-contre. Seuls les instruments polyphoniques peuvent jouer les accords plaqués : instruments à clavier (clavecin, orgue, piano, accordéon), les instruments à plusieurs cordes pincées (harpe, guitare ; violon, alto, violoncelle et contrebasse joués en pizzicati). Les instruments à corde frottés de la famille du violon peuvent jouer des notes par deux à l'archet mais pas plus du fait de la forme bombée du chevalet ; cependant, un mouvement rapide permet de jouer les quatre cordes de manière très rapprochée. Les instruments à percussion de type xylophone ou le tympanon permettent de jouer jusqu'à quatre notes simultanément en tenant deux baguettes (mailloches, maillets) par main. Tous les instruments peuvent jouer des arpèges même si, dans le cas des instruments monodiques, les notes ne continuent pas à sonner lorsque l'on passe à la note suivante. L'arpège peut être joué par l'instrument de basse (basson, violoncelle, contrebasse, guitare basse, pédalier de l'orgue…), notamment dans le cas d'une basse continue ou d'une ''{{lang|en|walking bass}}'' (« basse marchante » : la basse joue des noires, donnant ainsi l'impression qu'elle marche). En jazz, et spécifiquement au piano, on a recours au ''{{lang|en|voicing}}'' : on choisit la manière dont on organise les notes pour donner une couleur spécifique, ou bien pour créer une mélodie en enchaînant les accords. Il est fréquent de ne pas jouer toutes les notes : si on n'en garde que deux, ce sont la tierce et la septième, car ce sont celles qui caractérisent l'accord (selon que la tierce est mineure ou majeure, que la septième est majeure ou mineure), et la fondamentale est en général jouée par la contrebasse ou guitare basse. {{clear}} === Classes d'accord === [[Fichier:Intervalles harmoniques accords classes.svg|vignette|upright=1.5|Intervalles harmoniques dans les accords classés de trois, quatre et cinq notes.]] Un accord composé d'empilement de tierces est appelé « accord classé ». En musique tonale, c'est-à-dire la musique fondée sur les gammes majeures ou mineures (cas majoritaire en musique classique), on distingue trois classes d'accords : * les accords de trois notes, ou triades, ou accords de quinte ; * les accords de quatre notes, ou accords de septième ; * les accords de cinq notes, ou accords de neuvième. En empilant des tierces, si l'on part de la note fondamentale, on a donc de intervalles de tierce, quinte, septième et neuvième. En musique tonale, les accords avec d'autres intervalles (hors renversement, voir ci-après), typiquement seconde, quarte ou sixte, sont considérés comme des transitions entre deux accords classés. Ils sont appelés, selon leur utilisation, « accords à retard » (en anglais : ''{{lang|en|suspended chord}}'', accord suspendu) ou « appoggiature » (note « appuyée », étrangère à l'harmonie). Voir aussi plus loin la notion de note étrangère. === Renversements d'accords === [[File:Accord do majeur renversements.svg|thumb|Accord parfait de do majeur et ses renversements.]] [[Fichier:Progression dominante renverse parfait do majeur.svg|vignette|upright=0.6|Progression accord de dominante renversé → accord parfait en ''do'' majeur.]] Un accord classé est donc un empilement de tierces. Si l'on change l'ordre des notes, on a toujours le même accord mais il est fait avec d'autres intervalles harmoniques. Par exemple, l'accord parfait de ''do'' majeur dans son état fondamental, c'est-à-dire non renversé, s'écrit ''do'' - ''mi'' - ''sol''. Sa note fondamentale, ''do'', est aussi se note de basse. Si maintenant on prend le ''do'' de l'octave supérieure, l'accord devient ''mi - sol - do'' ; c'est l'empilement d'une tierce ''(mi - sol)'' et d'une quarte ''(sol - do)'', soit la superposition d'une tierce ''(mi - sol)'' et d'une sixième ''(mi - do)''. C'est le premier renversement de l'accord parfait de ''do'' majeur ; la fondamentale est toujours ''do'' mais la basse est ''mi''. Le second renversement est ''sol - do - mi''. L'utilisation de renversement peut faciliter l'exécution de la progression d'accord. Par exemple, en tonalité ''do'' majeur, si l'on veut passer de l'accord de dominante ''sol - si - ré'' à l'accord parfait ''do - mi - sol'', alors on peut utiliser le second renversement de l'accord de dominante : ''ré - sol - si'' → ''do - mi - sol''. Ainsi, la basse descend juste d'un ton ''(ré → do)'' et sur un piano, la main reste globalement dans la même position. Le renversement d'un accord permet également de respecter certaines règles de l'harmonie classique, notamment éviter que des voix se suivent strictement (« mouvement parallèle »), ce qui aurait un effet de platitude. De manière générale, la notion de renversement permet deux choses : * d'enrichir l'œuvre : pour créer une harmonie donnée (c'est-à-dire des sons sonnant bien ensemble), nous avons plus de souplesse, nous pouvons organiser ces notes comme nous le voulons selon les voix ; * de simplifier l'analyse : quelle que soit la manière dont sont organisées les notes, cela nous ramène à un même accord. {{citation bloc|Or il, y a plusieurs manières de jouer un accord, selon que l'on aborde par la première note qui le constitue, ''do mi sol'', la deuxième, ''mi sol do'', ou la troisième note, ''sol do mi''. Ce sont les renversements, [que Rameau] va classer en différentes combinaisons d'une seule matrice. Faisant cela, Rameau divise le nombre d'accords [de septième] par quatre. Il simplifie, il structure […].|{{ouvrage|prénom1=André |nom1=Manoukian |titre=Sur les routes de la musique |éditeur=Harper Collins |année=2021 |passage=54 |isbn=979-1-03391201-9}} }} {{clear}} [[File:Plusieurs realisation 1er renversement doM.svg|thumb|Plusieurs réalisation du premier renversement de l'accord de ''do'' majeur.]] Notez que # Les notes peuvent être répétées plusieurs fois, à différentes octaves. # Dans la notion de renversement, seule importe en fait la note de basse. Ainsi, les accords ''mi-sol-do'', ''mi-do-sol'', ''mi-do-mi-sol'', ''mi-sol-mi-do''… sont tous une déclinaison du premier renversement de ''do-mi-sol'' et ils seront abrégés de la même manière (''mi''<sup>6</sup> en musique classique ou C/E en musique populaire et jazz, voir plus bas). {{clear}} == Notation des accords de trois notes == Les accords de trois notes sont appelés « accords de quinte » en classique, et « triades » en jazz. [[Fichier:Progression dominante renverse parfait do majeur chiffrage.svg|vignette|upright=0.7|Chiffrage du second renversement d'un accord de ''sol'' majeur et d'un accord de ''do'' majeur : notation en musique populaire et jazz (haut) et notation de basse chiffrée (bas).]] Les accords sont construits de manière systématique. Nous pouvons donc les représenter de manière simplifiée. Cette notation simplifiée des accords est appelée « chiffrage ». Reprenons la progression d'accords ci-dessus : « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » dans la tonalité de ''do'' majeur. On utilise en général trois notations différentes : * en musique populaire, jazz, rock… un accord est désigné par sa note fondamentale ; ici donc, les accords sont notés « ''sol'' - ''do'' » ou, en notation anglo-saxonne, « G - C » ;<br /> comme le premier accord est renversé, on indique la note de basse après une barre, la progression d'accords est donc chiffrée '''« ''sol''/''ré'' - ''do'' »''' ou '''« G/D - C »''' ;<br /> il s'agit ici d'accords composés d'une tierce majeure et d'une quinte juste ; si les accords sont constitués d'intervalles différents, nous ajoutons un symbole après la note : « m » ou « – » si la tierce est mineure, « dim » ou « ° » si la quinte est diminuée ; * en musique classique, on utilise la notation de « basse chiffrée » (utilisée notamment pour noter la basse continue en musique baroque) : on indique la note de basse sur la portée et on lui adjoint l'intervalle de la fondamentale à la note la plus haute (donc ici respectivement 6 et 5, puisque ''sol''-''si'' est une sixte et ''do''-''sol'' est une quinte), étant sous-entendu que l'on a des empilements de tierce en dessous ; mais dans le cas du premier accord, le premier intervalle n'est pas une tierce, mais une quarte ''(ré''-''sol)'', on note donc '''« ''ré'' <sup>6</sup><sub>4</sub> - ''do'' <sup>5</sup> »'''<ref>quand on ne dispose pas de la notation en supérieur (exposant) et inférieur (indice), on utilise parfois une notation sous forme de fraction : ''sol'' 6/4 et ''do'' 5/.</ref> ; * lorsque l'on fait l'analyse d'un morceau, on s'attache à identifier la note fondamentale de l'accord (qui est différente de la basse dans le cas d'un renversement) ; on indique alors le degré de la fondamentale : '''« {{Times New Roman|V<sup>6</sup><sub>4</sub> - I<sup>5</sup>}} »'''. La notation de basse chiffrée permet de construire l'accord à la volée : * on joue la note indiquée (basse) ; * s'il n'y a pas de 2 ni de 4, on lui ajoute la tierce ; * on ajoute les intervalles indiqués par le chiffrage. La notation de musique jazz oblige à connaître la composition des différents accords, mais une fois que ceux-ci sont acquis, il n'y a pas besoin de reconstruire l'accord. La notation de basse chiffrée avec les chiffres romains n'est pas utilisée pour jouer, mais uniquement pour analyser ; Sur les partitions avec basse chiffrée, il y a simplement les chiffrages indiqués au-dessus de la partie de basse. Le chiffrage avec le degré en chiffres romains présente l'avantage d'être indépendant de la tonalité et donc de se concentrer sur la fonction de l'accord au sein de la tonalité. Par exemple, ci-dessous, nous pouvons parler de la progression d'accords « {{Times New Roman|V - I}} » de manière générale, cette notation étant valable quelle que soit la tonalité. [[File:Progression dominante renverse parfait do majeur chiffrage basse continue.svg|thumb|Chiffrage en notation basse chiffrée de la progression d'accords « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » en do majeur.]] {{note|En notation de base continue avec fondamentale en chiffres romains, la fondamentale est toujours indiquée ''sous'' la portée de la partie de basse. Les intervalles sont indiqués au-dessus de la portée de la partie de basse ; lorsque l'on fait une analyse, on peut ayssi les indiquer à côté du degré en chiffres romains, donc sous la portée de la basse.}} {{note|En notation rock, le 5 en exposant indique un accord incomplet avec uniquement la fondamentale et la quinte, un accord sans tierce appelé « accord de puissance » ou ''{{lang|en|power chord}}''. Par exemple, C<sup>5</sup> est l'accord ''do-sol''.}} {{clear}} [[Fichier:Accords parfait do majeur basse chiffree fondamental et renverse.svg|vignette|upright=2.5|Chiffrage de l'accord parfait de ''do'' majeur en basse chiffrée, à l'état fondamental et ses renversements.]] Concernant les accords parfaits en notation de basse chiffrée : * un accord parfait à l'état fondamental est chiffré « <sup>5</sup> » ; on l'appelle « accord de quinte » ; * le premier renversement est chiffré « <sup>6</sup> » (la tierce est implicite) ; on l'appelle « accord de sixte » ; * le second renversement est noté « <sup>6</sup><sub>4</sub> » ; on l'appelle « accord de sixte et de quarte » (ou bien « de quarte et de sixte »). Par exemple, pour l'accord parfait de ''do'' majeur : * l'état fondamental ''do''-''mi''-''sol'' est noté ''do''<sup>5</sup> ; * le premier renversement ''mi''-''sol''-''do'' est noté ''mi''<sup>6</sup> ; * le second renversement ''sol''-''do''-''mi'' est noté ''sol''<sup>6</sup><sub>4</sub>. Il y a une exception : l'accord construit sur la sensible (7{{e}} degré) contient une quinte diminuée et non une quinte juste. Le chiffrage est donc différent : * l'état fondamental ''si''-''ré''-''fa'' est noté ''si''<sup><s>5</s></sup> (cinq barré), « accord de quinte diminuée » ; * le premier renversement ''ré''-''fa''-''si'' est noté ''ré''<sup>+6</sup><sub>3</sub>, « accord de sixte sensible et tierce » ; * le second renversement ''fa''-''si''-''ré'' est noté ''fa''<sup>6</sup><sub>+4</sub>, « accord de sixte et quarte sensible ». Par ailleurs, on ne considère pas qu'il est fondé sur la sensible, mais sur la dominante. Si certains indiquent le degré {{Times New Roman|VII}}, d'autres indiquent le degré {{Times New Roman|V}} en mettant des guillemets, « {{Times New Roman|“V”}} ». Donc selon l'état, le chiffrage est {{Times New Roman|“V”<sup><s>5</s></sup>}}, {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}} ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}. En notation jazz, on ajoute « dim », « <sup>o</sup> » ou bien « <sup>♭5</sup> » au chiffrage, ici : B dim, B<sup>o</sup> ou B<sup>♭5</sup> pour l'état fondamental. Pour les renversements : B dim/D et B dim/F ; ou bien B<sup>o</sup>/D et B<sup>o</sup>/F ; ou bien B<sup>♭5</sup>/D et B<sup>♭5</sup>/F. {{clear}} [[Fichier:Accords basse chiffree basse do fondamental et renverses.svg|vignette|upright=2|Basse chiffrée : accords de quinte, de sixte et de sixte et de quarte ayant pour basse ''do''.]] Et concernant les accords ayant pour basse ''do'' en tonalité de ''do'' majeur : * l'accord ''do''<sup>5</sup> est un accord à l'état fondamental, c'est donc l'accord ''do''-''mi''-''sol'' (sa fondamentale est ''do'') ; * l'accord ''do''<sup>6</sup> est le premier renversement d'un accord, c'est donc l'accord ''do''-''mi''-''la'' (sa fondamentale est ''la'') ; * l'accord ''do''<sup>6</sup><sub>4</sub> est le second renversement d'un accord, c'est donc l'accord ''do''-''fa''-''la'' (sa fondamentale est ''fa''). {{clear}} == Notes étrangères == La musique européenne s'appuie essentiellement sur des accords parfaits, c'est-à-dire fondés sur une tierce majeure ou mineure, et une quinte juste. Il arrive fréquemment qu'un accord ne soit pas un accord parfait. Les notes qui font partie de l'accord parfait sont appelées « notes naturelles » et la note qui n'en fait pas partie est appelée « note étrangère ». Il existe plusieurs types de notes étrangères : * anticipation : la note étrangère est une note naturelle de l'accord suivant ; * appogiature : note d'ornementation qui se résout par mouvement conjoint, c'est-à-dire qu'elle est suivie par une note située juste au-dessus ou en dessous (seconde ascendante ou descendante) qui est, elle, une note naturelle ; * broderie : on part d'une note naturelle, on monte ou on descend d'une seconde, puis on revient sur la note naturelle ; * double broderie : on part d'une note naturelle, on joue la note du dessus puis la note du dessous avant de revenir à la note naturelle ; ou bien on joue la note du dessous puis la note du dessus ; * échappée : note étrangère n'appartenant à aucune des autres catégories ; * note de passage : mouvement conjoint allant d'une note naturelle d'un accord à une note naturelle de l'accord suivant ; * pédale : la note de basse reste la même pendant plusieurs accords successifs ; * retard : la note étrangère est une note naturelle de l'accord précédent. Les notes étrangères ne sont pas chiffrées. [[File:Notes etrangeres accords.svg|center|Différents types de notes étrangères.]] {{note|Les anglophones distinguent deux types de retard : la ''{{lang|en|suspension}}'' est résolue vers le haut (le mouvement est ascendant), le ''{{lang|en|retardation}}'' est résolu vers le bas (le mouvement est descendant).}} == Principaux accords == Les trois principaux accords sont : * l'accord parfait majeur : il est construit sur les degrés {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (médiante) et {{Times New Roman|V}} (dominante) d'une gamme majeure ; il est noté {{Times New Roman|I}}<sup>5</sup>, {{Times New Roman|IV}}<sup>5</sup>, {{Times New Roman|V}}<sup>5</sup> ; * l'accord parfait mineur : il est construit sur les degrés {{Times New Roman|I}} (tonique) et {{Times New Roman|IV}} (sous-tonique) d'une gamme mineure harmonique ; il est également noté {{Times New Roman|I}}<sup>5</sup> et {{Times New Roman|IV}}<sup>5</sup>, les anglo-saxons le notent {{Times New Roman|i}}<sup>5</sup> et {{Times New Roman|iv}}<sup>5</sup> (la minuscule indiquant le caractère mineur) ; * l'accord de septième de dominante : il est construit sur le degré {{Times New Roman|V}} (dominante) d'une gamme majeure ou mineure harmonique ; il est noté {{Times New Roman|V}}<sup>7</sup><sub>+</sub>. On peut trouver ces trois accords sur d'autres degrés, et il existe d'autre types d'accords. Nous verrons cela plus loin. {| class="wikitable" |+ Constitution des principaux accords — Dénomination classique |- ! scope="col" | Accord ! scope="col" | 1<sup>er</sup> intervalle ! scope="col" | 2<sup>e</sup> intervalle ! scope="col" | 3<sup>e</sup> intervalle |- ! scope="row" | Accord parfait majeur | tierce majeure (3M) || quinte juste (5J) || — |- ! scope="row" | Accord parfait mineur | tierce mineure (3m) || quinte juste (5J) || — |- ! scope="row" | Accord de septième de dominante | tierce majeure (3M) || quinte juste (5J) || septième mineure (7m) |} {| class="wikitable" |+ Constitution des principaux accords — Dénomination jazz |- ! scope="col" | Accord ! scope="col" | 1<sup>er</sup> intervalle ! scope="col" | 2<sup>e</sup> intervalle ! scope="col" | 3<sup>e</sup> intervalle |- ! scope="row" | Triade majeure | tierce majeure (3M) || quinte juste (5J) || — |- ! scope="row" | Triade mineure | tierce mineure (3m) || quinte juste (5J) || — |- ! scope="row" | Accord de septième | tierce majeure (3M) || quinte juste (5J) || septième mineure (7m) |} {| border="0" |- | [[Fichier:Accord do majeur arpege puis plaque.midi | Accord parfait de ''do'' majeur (C).]] || [[Fichier:Accord do mineur arpege puis plaque.midi | Accord parfait de ''do'' mineur (Cm).]] || [[Fichier:Accord do septieme arpege puis plaque.midi | Accord de septième de dominante de ''fa'' majeur (C<sup>7</sup>).]] |- | Accord parfait<br /> de ''do'' majeur (C). || Accord parfait<br /> de ''do'' mineur (Cm). || Accord de septième de dominante<br /> de ''fa'' majeur (C<sup>7</sup>). |} '''Rappel :''' * la tierce mineure est composée d'un ton et demi (1 t ½) ; * la tierce majeur est composée de deux tons (2 t) ; * la quinte juste a la même altération que la fondamentale, sauf lorsque la fondamentale est ''si'' (la quinte juste est alors ''fa''♯) ; * la septième mineure est le renversement de la seconde majeure (1 t). [[File:Renversements accords pft fa maj basse chiffree.svg|thumb|Renversements de l'accord parfait de ''fa'' majeur, et la notation de basse chiffrée.]] [[File:Renversements accord sept de dom fa maj basse chiffree.svg|thumb|Renversements de l'accord de septième de dominante de ''fa'' majeur, et la notation de basse chiffrée.]] {| class="wikitable" |+ Notation des principaux accords en musique classique |- ! scope="col" | Accord ! scope="col" | État<br /> fondamental ! scope="col" | Premier<br /> renversement ! scope="col" | Deuxième<br /> renversement ! scope="col" | Troisième<br /> renversement |- ! scope="row" | Accord parfait | {{Times New Roman|I<sup>5</sup>}}<br/> acc. de quinte || {{Times New Roman|I<sup>6</sup>}}<br :> acc. de sixte || {{Times New Roman|I<sup>6</sup><sub>4</sub>}}<br /> acc. de quarte et de sixte || — |- ! scope="row" | Accord de septième<br /> de dominante | {{Times New Roman|V<sup>7</sup><sub>+</sub>}}<br /> acc.de septième de dominante || {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}<br />acc. de sixte et quinte diminuée || {{Times New Roman|V<sup>+6</sup>}}<br />acc. de sixte sensible || {{Times New Roman|V<sup>+4</sup>}}<br />acc. de quarte sensible<br />acc. de triton |} {| class="wikitable" |+ Notation des principaux accords en jazz |- ! scope="col" | Accord ! scope="col" | Chiffrage ! scope="col" | Renversements |- ! scope="row" | Triade majeure | X | rowspan="3" | Les renversements se notent en mettant la basse après une barre de fraction, par exemple pour la triade de ''do'' majeur : * état fondamental : C ; * premier renversement : C/E ; * second renversement : C/G. |- ! scope="row" | Triade mineure | Xm, X– |- ! scope="row" | Septième | X<sup>7</sup> |} {{clear}} Dans le cas d'un accord de septième de dominante, le nom de l'accord change selon que l'on est en musique classique ou en jazz : en musique classique, on donne le nom de la tonalité alors qu'en jazz, on donne le nom de la fondamentale. Ainsi, l'accord appelé « septième de dominante de ''do'' majeur » en musique classique, est appelé « ''sol'' sept » (G<sup>7</sup>) en jazz : la dominante (degré {{Times New Roman|V}}, dominante) de la tonalité de ''do'' majeur est la note ''sol''. Comment appelle-t-on en musique classique l'accord appelé « ''do'' sept » (C<sup>7</sup>) en jazz ? Les tonalités dont le ''do'' est la dominante sont les tonalités de ''fa'' majeur (''si''♭ à la clef) et de ''fa'' mineur harmonique (''si''♭, ''mi''♭, ''la''♭ et ''ré''♭ à la clef et ''mi''♮ accidentel). Il s'agit donc de l'accord de septième de dominante des tonalités de ''fa'' majeur et ''fa'' mineur harmonique. {| class="wikitable" |+ Accords fréquents pour quelques la tonalités majeures ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Accord parfait<br />{{Times New Roman|I<sup>5</sup>}} ! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}} |- |''Do'' majeur || || C<br />''do-mi-sol'' || G7<br />''sol-si-ré-fa'' |- |''Sol'' majeur || ''fa''♯ || G<br />''sol-si-ré'' || D7<br />''ré-fa''♯''-la-do'' |- |''Ré'' majeur || ''fa''♯, ''do''♯ || D<br />''ré-fa''♯''-la'' || A7<br />''la-do''♯''-mi-sol'' |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A<br />''la-do''♯''-mi'' || E7<br />''mi-sol''♯''-si-ré'' |- | ''Fa'' majeur || ''si''♭ || F<br />''fa-la-do'' || C7<br />''do-mi-sol-si''♭ |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭<br />''si''♭''-ré-fa'' || F7<br />''fa-la-do-mi''♭ |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭<br />''mi''♭''-sol-si''♭ || B♭7<br />''si''♭''-ré-fa-la''♭ |} {| class="wikitable" |+ Accords fréquents pour quelques la tonalités mineures harmoniques ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Accord parfait<br />{{Times New Roman|i<sup>5</sup>}} ! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}} |- |''La'' mineur<br />harmonique || || Am, A–<br />''la-do-mi'' || E7<br />''mi-sol''♯''-si-ré'' |- |''Mi'' mineur<br />harmonique || ''fa''♯ || Em, E–<br />''mi-sol-si'' || B7<br />''si-ré''♯''-fa''♯''-la'' |- |''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || Bm, B–<br />''si-ré-fa''♯ || F♯7<br />''fa''♯''la''♯''-do''♯''-mi'' |- |''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || F♯m, F♯–<br />''fa''♯''-la-do''♯ || C♯7<br />''do''♯''-mi''♯''-sol''♯''-si'' |- | ''Ré'' mineur<br />harmonique || ''si''♭ || Dm, D–<br />''ré-fa-la'' || A7<br />''la-do''♯''-mi-sol'' |- | ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || Gm, G–<br />''sol-si''♭''-ré'' || D7<br />''ré-fa''♯''-la-do'' |- | ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || Cm, C–<br />''do-mi''♭''-sol'' || G7<br />''sol-si''♮''-ré-fa'' |} {{clear}} == Accords sur les degrés d'une gamme == === Harmonisation d'une gamme === [[Fichier:Accord trois notes gamme do majeur chiffre.svg|vignette|upright=1.2|Accords de trois note sur la gamme de ''do'' majeur, chiffrés.]] On peut ainsi construire une triade par degré d'une gamme. Pour une gamme majeure, les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|V<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|II<sup>5</sup>}}, {{Times New Roman|III<sup>5</sup>}}, {{Times New Roman|VI<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|ii<sup>5</sup>}}, {{Times New Roman|iii<sup>5</sup>}}, {{Times New Roman|vi<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}. Les accords ont tous une quinte juste à l'exception de l'accord {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} qui a une quinte diminuée, raison pour laquelle le « 5 » est barré. C'est un accord dit « de quinte diminuée ». En jazz, l'accord diminué est noté « dim », « ° », « m<sup>♭5</sup> » ou « <sup>–♭5</sup> ». Nous avons donc trois types d'accords (dans la notation jazz) : X (triade majeure), Xm (triade mineure) et X° (triade diminuée), la lettre X remplaçant le nom de la note fondamentale. {{clear}} [[Fichier:Accord trois notes gamme la mineur chiffre.svg|vignette|upright=1.2|Accords de trois notes sur une gamme de ''la'' mineur harmonique, chiffrés.]] Pour une gamme mineure harmonique, les accords {{Times New Roman|III<sup>+5</sup>}}, {{Times New Roman|V<sup>♯</sup>}} et {{Times New Roman|VI<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|II<sup><s>5</s></sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|i<sup>5</sup>}}, {{Times New Roman|ii<sup><s>5</s></sup>}}, {{Times New Roman|iv<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}. Les accords {{Times New Roman|ii<sup><s>5</s></sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} ont une quinte diminuée ; ce sont des accords dits « de quinte diminuée ». L'accord {{Times New Roman|III<sup>+5</sup>}} a une quinte augmentée ; le signe « plus » indique que la note de cinquième, le ''sol'' dièse, est la sensible. En jazz, l'accord est noté « aug » ou « <sup>+</sup> ». Les autres accords ont une quinte juste. Aux trois accords générés par une gamme majeure (X, Xm et X°), nous voyons ici apparaître un quatrième type d'accord : la triade augmentée X<sup>+</sup>. Nous remarquons que des gammes ont des accords communs. Par exemple, l'accord {{Times New Roman|ii<sup>5</sup>}} de ''do'' majeur est identique à l'accord {{Times New Roman|iv<sup>5</sup>}} de ''la'' mineur (il s'agit de l'accord Dm). Quel que soit le mode, les accords construits sur la sensible (accord de quinte diminuée) sont rarement utilisés. S'ils le sont, c'est en tant qu'accord de septième de dominante sans fondamentale (voir ci-après). C'est la raison pour laquelle le chiffrage indique le degré {{Times New Roman|V}} entre guillemets, et non pas le degré {{Times New Roman|VII}} (mais pour des raisons de clarté, nous l'indiquons entre parenthèses au début). En mode mineur, l'accord de quinte augmentée {{Times New Roman|iii<sup>+5</sup>}} est très peu utilisé (voir plus loin ''[[#Progression_d'accords|Progression d'accords]]''). C'est un accord considéré comme dissonant. On voit que : * un accord parfait majeur peut appartenir à cinq gammes différentes ;<br /> par exemple l'accord parfait de ''do'' majeur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> degré de la gamme de ''do'' majeur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' mineur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''mi'' mineur ; * un accord parfait mineur peut appartenir à cinq gammes différentes ;<br />par exemple l'accord parfait de ''la'' mineur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> de la gamme de ''la'' mineur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''mi'' mineur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|III}}<sup>e</sup> degré de ''fa'' majeur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''do'' majeur ; * un accord de quinte diminuée peut appartenir à trois gammes différentes ;<br />par exemple, l'accord de quinte diminuée de ''si'' est l'accord construit sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' majeur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''la'' mineur et sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' mineur ; * un accord de quinte augmentée (à l'état fondamental) ne peut appartenir qu'à une seule gamme ;<br /> par exemple, l'accord de quinte augmentée de ''do'' est l'accord construit sur le {{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur. {| class="wikitable" |+ Notation jazz des triades |- | rowspan="2" colspan="2" | ! scope="col" colspan="2" | Tierce |- ! scope="col" | 3m ! scope="col" | 3M |- ! rowspan="3" | Quinte ! scope="row" | 5d | Xᵒ, X<sub>m</sub><sup>(♭5)</sup> || |- ! scope="row" | 5J | Xm, X– || X |- ! scope="row" | 5A | || X+, X<sup>(♯5)</sup> |} === Harmonisation par des accords de septième === [[Fichier:Harmonisation gamme do majeur par septiemes chiffre.svg|vignette|upright=2|Harmonisation de la gamme de do majeur par des accords de septième.]] Les accords de septième contiennent une dissonance et créent ainsi une tension. Ils sont très utilisés en jazz. Nous avons représenté ci-contre l'harmonisation de la gamme de ''do'' majeur. La constitution des accords est la suivantes : * tierce majeure (3M) ** quinte juste (5J) *** septième mineure (7m) : sur le degré V, c'est l'accord de septième de dominante V<sup>7</sup><sub>+</sub>, noté X<sup>7</sup> (X pour G), *** septième majeure (7M) : sur les degrés I et IV, appelés « accords de septième majeure » et notés aussi X<sup>maj7</sup> ou X<sup>Δ</sup> (X pour C ou F) ; * tierce mineure (3m) ** quinte juste (5J) *** septième mineure : sur les degrés ii, iii et vi, appelés « accords mineur septième » et notés Xm<sup>7</sup> ou X–<sup>7</sup> (X pour D, E ou A), ** quinte diminuée (5d) *** septième mineure (7m) : sur le degré vii, appelé « accord demi-diminué » (puisque seule la quinte est diminuée) et noté X<sup>∅</sup> ou Xm<sup>7(♭5)</sup> ou X–<sup>7(♭5)</sup> (X pour B) ;<br /> en musique classique, on considère que c'est un accord de neuvième de dominante sans fondamentale. Nous avons donc quatre types d'accords : X<sup>7</sup>, X<sup>maj7</sup>, Xm<sup>7</sup> et X<sup>∅</sup> En jazz, on ajoute souvent la quarte à l'accord de sous-dominante IV (sur le ''fa'' dans une gamme de ''do'' majeur) ; il s'agit ici d'une quarte augmentée (''fa''-''si'') et l'accord est surnommé « accord lydien » mais cette dénomination est erronée (il s'agit d'une mauvaise interprétation de textes antiques). C'est un accord de onzième sans neuvième (la onzième étant l'octave de la quarte), il est noté X<sup>maj7(♯11)</sup> ou X<sup>Δ(♯11)</sup> (ici, F<sup>maj7(♯11)</sup>, ''fa''-''la''-''do''-''mi''-''si'' ou ''fa''-''la''-''si''-''do''-''mi''). {| class="wikitable" |+ Chiffrage jazz des accords de septième |- ! scope="col" rowspan="2" | Tierce ! scope="col" rowspan="2" | Quinte ! scope="col" colspan="2" | Septième |- ! scope="col" | 7m ! scope="col" | 7M |- | rowspan="2" | 3m | 5d || X<sup>∅</sup>, X<sub>m</sub><sup>7(♭5)</sup>, X–<sup>7(♭5)</sup> || |- | rowspan="2" | 5J | X<sub>m</sub><sup>7</sup>, X–<sup>7</sup> || |- | rowspan="2" | 3M | X<sup>7</sup> || X<sup>maj7</sup>, X<sup>Δ</sup> |- | 5A || || X<sub>+</sub><sup>maj7</sup>, X<sub>+</sub><sup>Δ</sup> |} === Modulation et emprunt === Un morceau peut comporter des changements de tonalité; appelés « modulation ». Il y a parfois un court passage dans une tonalité différente, typiquement sur une ou deux mesures, avant de retourner dans la tonalité d'origine : on parle d'emprunt. Lorsqu'il y a une modulation ou un emprunt, les degrés changent. Un même accord peut donc avoir une fonction dans une partie du morceau et une autre fonction ailleurs. L'utilisation d'accord différents, et en particulier d'accord utilisant des altérations accidentelles, indique clairement une modulation. Nous avons vu précédemment que les modulations courantes sont : * les modulations dans les tons voisins ; * les modulations homonymes ; * les marches harmoniques. Une modulation entre une tonalité majeure et mineure change la couleur du passage, * la modulation la plus « douce » est entre les tonalités relatives (par exemple''do'' majeur et ''la'' mineur) car ces tonalités utilisent quasiment les mêmes notes ; * la modulation la plus « voyante » est la modulation homonyme (par exemple entre ''do'' majeur et ''do'' mineur). Une modulation commence souvent sur l'accord de dominante de la nouvelle tonalité. Pour analyser un œuvre, ou pour improviser sur une partie, il est important de reconnaître les modulations. La description de la successind es tonalités s'appelle le « parcours tonal ». === Exercices élémentaires === L'apprentissage des accords passe par quelques exercices élémentaires. '''1. Lire un accord''' Il s'agit de lecture de notes : des notes composant les accords sont écrites « empilées » sur une portée, il faut les lire en énonçant les notes de bas en haut. '''2. Reconnaître la « couleur » d'un accord''' On écoute une triade et il faut dire si c'est une triade majeure ou mineure. Puis, on complexifie l'exercice en ajoutant la septième. '''3. Chiffrage un accord''' Trouver le nom d'un accord à partir des notes qui le composent. '''4. Réalisation d'un accord''' Trouver les notes qui composent un accord à partir de son nom. '''5. Dictée d'accords''' On écoute une succession d'accords et il faut soit écrire les notes sur une portée, soit écrire les noms de accords. [[File:Exercice constitution accord basse chiffree.svg|thumb|Exercice : constitution d'accord à partir de la basse chiffrée.]] '''Exercices de basse chiffrée''' ''Réalisation d'un accord'' Sur la figure suivante, écrire les notes des accords correspondant à la basse chiffrée. Déterminer le degré de la fondamentale pour chaque accord en considérant que nous sommes dans la tonalité de ''sol'' majeur. {{boîte déroulante/début|titre=Solution}} [[File:Exercice constitution accord basse chiffree solution.svg|vignette|Solution.]] # La note de basse est un ''do''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''mi'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''sol''.<br />Le chiffrage « <sup>5</sup> » indique que c'est un accord dans son état fondamental (l'écart entre deux notes consécutives ne dépasse pas la tierce), la fondamentale est donc la basse, ''do'', qui est le degré IV de la tonalité. # La note de basse est un ''si''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''ré'', puis nous appliquons le chiffrage 6 et ajoutons la sixte, ''sol''.<br />Le chiffrage « <sup>6</sup> » indique que c'est un accord dans son premier renversement. En le remettant dans son état fondamental, nous obtenons ''sol-si-ré'', la fondamentale est donc la tonique, le degré I. # La note de basse est un ''la''. Nous ajoutons la tierce (chiffre 3), ''do'', et la sixte (6), ''fa''♯. Nous vérifions que le ''fa''♯ est la sensible (signe +)<br />Nous voyons un « blanc » entre les notes ''do'' et ''fa''♯. En descendant le ''fa''♯ à l'octave inférieure, nous obtenons un empilement de tierces ''fa''♯-''la-do'', le fondamentale est donc ''fa''♯, le degré VII. Nous pouvons le voir comme le deuxième renversement de l'accord de septième de dominante, sans fondamentale. # La note de basse est un ''fa''♯. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''la'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''do'' ; nous vérifions qu'il s'agit bien d'une quinte diminuée (le 5 est barré). Nous appliquons le chiffre 6 et ajoutons la sixte, ''ré''.<br />Nous voyons que les notes ''do'' et ''ré'' sont conjointes (intervalle de seconde). En descendant le ''ré'' à l'octave inférieure, nous obtenons un empilement de tierces ''ré-fa''♯-''la-do'', le fondamentale est donc ''ré'', le degré V. Nous constatons que l'accord chiffré est le premier renversement de l'accord de septième de dominante. {{boîte déroulante/fin}} {{clear}} ''Chiffrage d'accords'' [[Fichier:Exercice chiffrage accord basse chiffree.svg|vignette|Accords à chiffrer.]] Chiffrer les accords ci-contre. {{boîte déroulante/début|titre=Solution}} [[File:Exercice chiffrage accord basse chiffree solution.svg|vignette|Solution.]] # On relève les intervalles en partant de la basse : tierce majeure (3M) et quinte juste (5J). Le chiffrage complet est donc ''fa''<sup>5</sup><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''fa''<sup>5</sup>.<br /> On peut aussi reconnaître que c'est l'accord parfait sur la tonique de la tonalité de ''fa'' majeur dans son état fondamental, le chiffrage d'un accord parfait étant <sup>5</sup>. # On relève les intervalles en partant de la basse : quarte juste (4J), sixte majeure (6M). Le chiffrage complet est donc ''fa''<sup>6</sup><sub>4</sub>.<br /> On peut aussi reconnaître que c'est le second renversement de l'accord ''mi-sol-si'', sur la tonique de la tonalité de ''mi'' mineur, le chiffrage du second renversement d'un accord parfait étant <sup>6</sup><sub>4</sub>. # Les intervalles en partant de la basse sont : tierce mineure (3m), quinte diminuée (5d), sixte mineure (6m). Le chiffrage complet est donc ''mi''<sup>6</sup><small><s>5</s></small><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''mi''<sup>6</sup><sub><s>5</s></sub>.<br /> On reconnaît le premier renversement de l'accord ''do-mi-sol-si''♭, accord de septième de dominante de la tonalité de ''fa'' majeur. # Les intervalles en partant de la basse sont : tierce mineure (3m), quinte juste (5J), septième mineure (7m). Le chiffrage complet est donc ''ré''<sup>7</sup><small>5</small><sub>3</sub> ; c'est typique d'un accord de septième de dominante, son chiffrage est donc ''ré''<sup>7</sup><sub>+</sub>.<br /> On reconnaît l'accord de septième de dominante de la tonalité de ''sol'' mineur dans son état fondamental. {{boîte déroulante/fin}} {{clear}} [[File:Exercice constitution accord notation jazz.svg|thumb|Exercice : constitution d'un accord d'après son chiffrage en notation jazz.]] '''Exercices de notation jazz''' ''Réalisation d'un accord'' Sur la figure suivante, écrire les notes des accords correspondant aux chiffrages. {{boîte déroulante/début|titre=Solution}} [[File:Exercice constitution accord notation jazz solution.svg|thumb|Solution.]] # Il s'agit de la triade majeure de ''do'' dans son état fondamental. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''do-mi-sol''. # Il s'agit de la triade majeure de ''sol''. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''sol-si-ré''. On renverse l'accord afin que la basse soit le ''si'', l'accord est donc ''si-ré-sol''. # Il s'agit de l'accord demi-diminué de ''fa''♯. Les intervalles sont la tierce mineure (3m), la quinte diminuée (5d) et la septième mineure (7m). Les notes sont donc ''fa''♯-''la-do-mi''. Nous renversons l'accord afin que la basse soit le ''la'', l'accord est donc ''a-do-mi-fa''♯. # Il s'agit de l'accord de septième de ''ré''. Les intervalles sont donc la tierce majeure (3M), la quinte juste (5J) et la septième mineure (7m). Les notes sont ''ré-fa''♯''-la-do''. Nous renversons l'accord afin que la basse soit le ''fa''♯, l'accord est donc ''fa''♯''-la-do-ré''. {{boîte déroulante/fin}} {{clear}} ''Chiffrage d'accords'' [[File:Exercice chiffrage accord notation jazz.svg|thumb|Accords à chiffrer.]] Chiffrer les accords ci-contre. {{boîte déroulante/début|titre=Solution}} [[File:Exercice chiffrage accord notation jazz solution.svg|thumb|Solution.]] # Les notes sont toutes sur des interlignes consécutifs, c'est donc un empilement de tierces ; l'accord est dans son état fondamental. Les intervalles sont une tierce majeure (''fa-la'' : 3M) et une quinte juste (''fa-do'' : 5J), c'est donc la triade majeure de ''fa''. Le chiffrage est F. # Il y a un blanc dans l'empilement des notes, c'est donc un accord renversé. En permutant les notes pour n'avoir que des tierces, on trouve l'accord ''mi-sol-si''. Les intervalles sont une tierce mineure (''mi-sol'' : 3m) et une quinte juste (''mi-si'' : 5J), c'est donc la triade mineure de ''mi'' avec un ''si'' à la basse. Le chiffrage est Em/B ou E–/B. # Il y deux notes conjointes, c'est donc un renversement. L'état fondamental de cet accord est ''do-mi-sol-si''♭. Les intervalles sont une tierce majeure (''do-mi'' : 3M), une quinte juste (''do-sol'' : 5J) et une septième mineure (''do-si''♭ : 7m). C'est donc l'accord de ''do'' septième avec un ''mi'' à la basse, chiffré C<sup>7</sup>/E. # Les notes sont toutes sur des interlignes consécutifs, l'accord est dans son état fondamental. Les intervalles sont la tierce mineure (''ré-fa'' : 3m), une quinte juste (''ré-la'' : 5J) et une septième mineure (''ré-do'' : 7m). C'est donc l'accord de ''ré'' mineur septième, chiffré Dm<sup>7</sup> ou D–<sup>7</sup>. {{boîte déroulante/fin}} {{clear}} == Harmonie fonctionnelle == Le choix des accords et de leur succession — la progression des accords — est un élément important d'un morceau, de sa composition. Le compositeur ou la compositrice a bien sûr une liberté totale, mais pour faire des choix, il faut comprendre les conséquences de ces choix, et donc ici, les effets produits par les accords et leur progression. Une des manières d'aborder le sujet est l'harmonie fonctionnelle. === Les trois fonctions des accords === En harmonie tonale, on considère que les accords ont une fonction. Il existe trois fonctions : * la fonction de tonique, {{Times New Roman|I}} ; * la fonction de sous-dominante, {{Times New Roman|IV}} ; * la fonction de dominante, {{Times New Roman|V}}. L'accord de tonique, {{Times New Roman|I}}, est l'accord « stable » de la tonalité par excellence. Il conclut en général les morceaux, et ouvre souvent les morceaux ; il revient fréquemment au cours du morceau. L'accord de dominante, {{Times New Roman|V}}, est un accord qui introduit une instabilité, une tension. En particulier, il contient la sensible (degré {{Times New Roman|VI}}), qui est une note « aspirée » vers la tonique. Cette tension, qui peut être renforcée par l'utilisation d'un accord de septième, est fréquemment résolue par un passage vers l'accord de tonique. Nous avons donc deux mouvements typiques : {{Times New Roman|I}} → {{Times New Roman|V}} (création d'une tension, d'une attente) et {{Times New Roman|V}} → {{Times New Roman|I}} (résolution d'une tension). Les accords de tonique et de dominante ont le cinquième degré en commun, cette note sert donc de pivot entre les deux accords. L'accord de sous-dominante, {{Times New Roman|IV}}, est un accord qui introduit lui aussi une tension, mais moins grande : il ne contient pas la sensible. Notons que s'il est une quarte au-dessus de la tonique, il est aussi une quinte en dessous d'elle ; il est symétrique de l'accord de dominante. Il a donc un rôle similaire à l'accord de dominante, mais atténué. L'accord de sous-dominante aspire soit vers l'accord de dominante, très proche, et l'on a alors une augmentation de la tension ; soit vers l'accord de tonique, un retour vers la stabilité (il a alors un rôle semblable à la dominante). Du fait de ces deux bifurcations possibles — augmentation de la tension ({{Times New Roman|IV}} → {{Times New Roman|V}}) ou retour à la stabilité ({{Times New Roman|IV}} → {{Times New Roman|I}}) —, l'utilisation de l'accord de sous-dominante introduit un certain flottement : si l'on peut facilement prédire l'accord qui suit un accord de dominante, on ne peut pas prédire ce qui suit un accord de sous-dominante. Notons que la composition ne consiste pas à suivre ces règles de manière stricte, ce qui conduirait à des morceaux stéréotypés et plats. Le plaisir d'écoute joue sur une alternance entre satisfaction d'une attente (respect des règles) et surprise (rompre les règles). === Accords remplissant ces fonctions === Les accords sur les autres degrés peuvent se ramener à une de ces trois fonctions : * {{Times New Roman|II}} : fonction de sous-dominante {{Times New Roman|IV}} ; * {{Times New Roman|III}} (très peu utilisé en mode mineur en raison de sa dissonance) et {{Times New Roman|VI}} : fonction de tonique {{Times New Roman|I}} ; * {{Times New Roman|VII}} : fonction de dominante {{Times New Roman|V}}. En effet, les accords étant des empilements de tierces, des accords situés à une tierce l'un de l'autre — {{Times New Roman|I}} ↔ {{Times New Roman|III}}, {{Times New Roman|II}} ↔ {{Times New Roman|IV}}, {{Times New Roman|V}} ↔ {{Times New Roman|VII}}, {{Times New Roman|VI}} ↔ {{Times New Roman|VIII}} ( = {{Times New Roman|I}}) — ont deux notes en commun. On retrouve le fait que l'accord sur le degré {{Times New Roman|VII}} est considéré comme un accord de dominante sans tonique. En mode mineur, l'accord sur le degré {{Times New Roman|III}} est évité, il n'a donc pas de fonction. {|class="wikitable" |+ Fonction des accords |- ! scope="col" | Fondamentale ! scope="col" | Fonction |- | {{Times New Roman|I}} || tonique |- | {{Times New Roman|II}} || sous-dominante faible |- | {{Times New Roman|III}} || tonique faible |- | {{Times New Roman|IV}} || sous-dominante |- | {{Times New Roman|V}} || dominante |- | {{Times New Roman|VI}} || tonique faible |- | {{Times New Roman|VII}} || dominante faible |} Par exemple en ''do'' majeur : * fonction de tonique : '''''do''<sup>5</sup> (C)''', ''mi''<sup>5</sup> (E–), ''la''<sup>5</sup> (A–) ; * fonction de sous-dominante : '''''fa''<sup>5</sup> (F)''', ''ré''<sup>5</sup> (D–) ; * fonction de dominante : '''''sol''<sup>5</sup> (G)''' ou ''sol''<sup>7</sup><sub>+</sub> (G<sup>7</sup>), ''si''<sup> <s>5</s></sup> (B<sup>o</sup>). En ''la'' mineur harmonique : * fonction de tonique : '''''la''<sup>5</sup> (A–)''', ''fa''<sup>5</sup> (F) [, rarement : ''do''<sup>+5</sup> (C<sup>+</sup>)] ; * fonction de sous-dominante : '''''ré''<sup>5</sup> (D–)''', ''si''<sup> <s>5</s></sup> (B<sup>o</sup>) ; * fonction de dominante : '''''mi''<sup>5</sup> (E)''' ou ''mi''<sup>7</sup><sub>+</sub> (E<sup>7</sup>), ''sol''♯<sup> <s>5</s></sup> (G♯<sup>o</sup>). Le fait d'utiliser des accords différents pour remplir une fonction permet d'enrichir l'harmonie, et de jouer sur l'équilibre entre satisfaction d'une attente (on respecte les règles sur les fonctions) et surprise (mais on n'utilise pas l'accord attendu). === Les dominantes secondaires === On utilise aussi des accords de septième dominante se fondant sur un autre degré que la dominante de la gamme ; on parle de « dominante secondaire ». Typiquement, avant un accord de septième de dominante, on utilise parfois un accord de dominante de dominante, dont le degré est alors noté « {{Times New Roman|V}} de {{Times New Roman|V}} » ou « {{Times New Roman|V}}/{{Times New Roman|V}} » ; la fondamentale est de l'accord est alors situé cinq degrés au-dessus de la dominante ({{Times New Roman|V}}), c'est donc le degré {{Times New Roman|IX}}, c'est-à-dire le degré {{Times New Roman|II}} de la tonalité en cours). Ou encore, on utilise un accord de dominante du degré {{Times New Roman|IV}} (« {{Times New Roman|V}} de {{Times New Roman|IV}} », la fondamentale est alors le degré {{Times New Roman|I}}) avant un accord sur le degré {{Times New Roman|IV}} lui-même. Par exemple, en tonalité de ''do'' majeur, on peut trouver un accord ''ré - fa''♯'' - la - do'' (chiffré {{Times New Roman|V}} de {{Times New Roman|V}}<sup>7</sup><sub>+</sub>), avant un accord ''sol - si - ré - fa'' ({{Times New Roman|V}}<sup>7</sup><sub>+</sub>). L'accord ''ré - fa''♯'' - la - do'' est l'accord de septième de dominante des tonalités de ''sol''. Dans la même tonalité, on pourra utiliser un accord ''do - mi - sol - si''♭ ({{Times New Roman|V}} de {{Times New Roman|IV}}<sup>7</sup><sub>+</sub>) avant un accord ''fa - la - do'' ({{Times New Roman|IV}}<sup>5</sup>). Le recours à une dominante secondaire peut atténuer une transition, par exemple avec un enchaînement ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> → ''fa''<sup>5</sup> (C → C<sup>7</sup> → F) qui correspond à un enchaînement {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}} : le passage ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> (C → C<sup>7</sup>) se fait en ajoutant une note (le ''si''♭) et rend naturel le passage ''do'' → ''fa''. Sur les sept degré de la gamme, on ne considère en général que cinq dominantes secondaires : en effet, la dominante du degré {{Times New Roman|I}} est la dominante « naturelle, primaire » de la tonalité (et n'est donc pas secondaire) ; et utiliser la dominante de {{Times New Roman|VII}} consisterait à considérer l'accord de {{Times New Roman|VII}} comme un accord propre, on évite donc les « {{Times New Roman|V}} de “{{Times New Roman|V}}” » (mais les « “{{Times New Roman|V}}” de {{Times New Roman|V}} » sont tout à fait « acceptables »). === Enchaînements classiques === Nous avons donc vu que l'on trouve fréquemment les enchaînements suivants : * pour créer une instabilité : ** {{Times New Roman|I}} → {{Times New Roman|V}}, ** {{Times New Roman|I}} → {{Times New Roman|IV}} (instabilité moins forte mais incertitude sur le sens d'évolution) ; * pour maintenir l'instabilité : ** {{Times New Roman|IV}} → {{Times New Roman|V}} ; * pour résoudre l'instabilité : ** {{Times New Roman|IV}} → {{Times New Roman|I}}, ** {{Times New Roman|V}} → {{Times New Roman|I}}, cas particuliers (voir plus bas) : *** {{Times New Roman|V}}<sup>+4</sup> → {{Times New Roman|I}}<sup>6</sup>, *** {{Times New Roman|I}}<sup>6</sup><sub>4</sub> → {{Times New Roman|V}}<sup>7</sup><sub>+</sub> → {{Times New Roman|I}}<sup>5</sup>. Les degrés indiqués ci-dessus sont les fonctions ; on peut donc utiliser les substitutions suivantes : * {{Times New Roman|I}} par {{Times New Roman|VI}} et, en tonalité majeure, {{Times New Roman|III}} ; * {{Times New Roman|IV}} par {{Times New Roman|II}} ; * {{Times New Roman|V}} par {{Times New Roman|VII}}. Pour enrichir l'harmonie, on peut utiliser les dominantes secondaires, en particulier : * {{Times New Roman|V}} de {{Times New Roman|V}} ({{Times New Roman|II}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|V}}, * {{Times New Roman|V}} de {{Times New Roman|IV}} ({{Times New Roman|I}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|IV}}. On peut enchaîner les enchaînements, par exemple {{Times New Roman|I}} → {{Times New Roman|IV}} → {{Times New Roman|V}}, ou encore {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}}… En jazz, on utilise très fréquemment l'enchaînement {{Times New Roman|II}} → {{Times New Roman|V}} → {{Times New Roman|I}} (deux-cinq-un). On peut bien sûr avoir d'autres enchaînements, mais ces règles permettent d'analyser un grand nombre de morceaux, et donnent des clefs utiles pour la composition. Nous voyons ci-après un certain nombre d'enchaînements courants dans différents styles == Exercice == Un hautboïste travaille la sonate en ''do'' mineur S. 277 de Heinichen. Sur le deuxième mouvement ''Allegro'', il a du mal à travailler un passage en raison des altérations accidentelles. Sur la suggestion de sa professeure, il décide d'analyser la progression d'accords sous-jacente afin que les altérations deviennent logiques. Il s'agit d'un duo hautbois et basson pour lequel les accords ne sont pas chiffrés, le basson étant ici un instrument soliste et non pas un élément de la basse continue. Sur l'extrait suivant, déterminez les basses et la qualité (chiffrage) des accords sous-jacents. Commentez. [[Fichier:Sonate hautbois basson heinichen 2e mvt mes49.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen.]] {{note|L'œuvre est en ''do'' mineur et devrait donc avoir trois bémols à la clef, or ici il n'y en a que deux. En effet, le ''la'' pouvant être bécarre en mode mineur mélodique ascendant, le compositeur a préféré le noter explicitement en altération accidentelle lorsque l'on est en mode mélodique naturel, harmonique ou mélodique descendant. C'est un procédé assez courant à l'époque baroque.}} {{boîte déroulante/début|titre=Solution}} Une des difficultés ici est que les arpèges joués par les instruments sont agrémentés de notes de passage. Les notes de la basse (du basson) sont différentes entre le premier et le deuxième temps de chaque mesure et ne peuvent pas appartenir au même accord. On a donc un accord par temps. Sur le premier temps de chaque mesure, le basson joue une octave. La note concernée est donc la basse de chaque accord. Pour savoir s'il s'agit d'un accord à l'état fondamental ou d'un renversement, on regarde ce que joue le hautbois : dans un mouvement conjoint (succession d'intervalles de secondes), il est difficile de distinguer les notes de l'arpège des notes de passage, mais : les notes des grands intervalles font partie de l'accord. Ainsi, sur le premier temps de la première mesure (la basse est un ''mi''♭), on a une sixte descendante ''sol''-''si''♭ et, à la fin du temps, une tierce descendante ''sol''-''mi''♭. L'accord est donc ''mi''♭-''sol''-''si''♭, c'est un accord de quinte (accord parfait à l'état fondamental). À la fin du premier temps, le basson joue un ''do'', c'est donc une note étrangère. Sur le second temps de la première mesure, le basson joue une tierce ascendante ''fa''-''la''♭, la première note est la basse de l'accord et la seconde une des notes de l'accord. Le hautbois commence par une sixte descendante ''la''♭-''do'', l'accord est donc ''fa''-''la''♭-''do'', un accord de quinte (accord parfait à l'état fondamental). Le ''do'' du basson la fin du premier temps est donc une anticipation. Les autres notes étrangères de la première mesure sont des notes de passage. Mais il faut faire attention : en suivant ce principe, sur les premiers temps des deuxième et troisième mesure, nous aurions des accords de septième d'espèce (puisque la septième est majeure). Or, on ne trouve pas, ou alors exceptionnellement, d'accord de septième d'espèce dans le baroque, mais quasi exclusivement des accords de septième de dominante. Donc au début de la deuxième mesure, le ''la''♮ est une appoggiature du ''si''♭, l'accord est donc ''si''♭-''ré''-''fa'', un asscord de quinte. De même, au début de la troisième mesure, le ''sol'' est une appoggiature du ''la''♭. Il faut donc se méfier d'une analyse purement « mathématique ». Il faut s'attacher à ressentir la musique, et à connaître les styles, pour faire une analyse pertinente. Ci-dessous, nous avons grisé les notes étrangères. [[Fichier:Sonate hautbois basson heinichen 2e mvt mes49 analyse.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen. Analyse de la progression harmonique.]] Le chiffrage jazz équivalent est : : | E♭ F– | B♭<sup>Δ</sup> E♭ | A♭<sup>Δ</sup> D– | G … Nous remarquons une progression assez régulière : : ''mi''♭ ↗[2<sup>de</sup>] ''fa'' | ↘[5<sup>te</sup>] ''si''♭ ↗[4<sup>te</sup>] ''mi''♭ | ↘[5<sup>te</sup>] ''la''♭ ↗[4<sup>te</sup>] ''ré'' | ↘[5<sup>te</sup>] ''sol'' Le ''mi''♭ est le degré {{Times New Roman|III}} de la tonalité principale (''do'' mineur), c'est donc une tonique faible ; il « joue le même rôle » qu'un ''do''. S'il y avait eu un accord de ''do'' au début de l'extrait, on aurait eu une progression parfaitement régulière ↗[4<sup>te</sup>] ↘[5<sup>te</sup>]. Nous avons les modulations suivantes : * mesure 49 : ''do'' mineur naturel (le ''si''♭ n'est pas une sensible) avec un accord sur “{{Times New Roman|I}}” (tonique faible, {{Times New Roman|III}}, pour la première analyse, ou bien tonique forte, {{Times New Roman|I}}, pour la seconde) suivi d'un accord sur {{Times New Roman|IV}} ; * mesure 50 : ''si''♭ majeur avec un accord sur {{Times New Roman|I}} suivi d'un accord sur {{Times New Roman|IV}} ; * mesure 51 : ''la''♭ majeur avec un accord sur {{Times New Roman|I}}, et emprunt à ''do'' majeur avec un accord sur {{Times New Roman|II}} ({{Times New Roman|IV}} faible). On a donc une marche harmonique {{Times New Roman|I}} → {{Times New Roman|IV}} qui descend d'une seconde majeure (un ton) à chaque mesure (''do'' → ''si''♭ → ''la''♭), avec une exception sur la dernière mesure (modulation en cours de mesure et descente d'une seconde mineure au lieu de majeure). Ce passage est donc construit sur une régularité, une règle qui crée un effet d'attente — enchaînement {{Times New Roman|I}}<sup>5</sup> → {{Times New Roman|IV}}<sup>5</sup> avec une marche harmonique d'une seconde majeure descendante —, et des « surprises », des exceptions au début — ce n'est pas un accord {{Times New Roman|I}}<sup>5</sup> mais un accord {{Times New Roman|III}}<sup>5</sup> — et à la fin — modulation en milieu de mesure et dernière descente d'une seconde mineure (½t ''la''♭ → ''sol''). L'extrait ne permet pas de le deviner, mais la mesure 52 est un retour en ''do'' mineur, avec donc une modulation sur la dominante (accord de ''sol''<sup>7</sup><sub>+</sub>, G<sup>7</sup>). {{boîte déroulante/fin}} == Progression d'accords == Comme pour la mélodie, la succession des accords dans un morceau, la progression d'accords, suit des règles. Et comme pour la mélodie, les règles diffèrent d'un style musical à l'autre et la créativité consiste à parfois ne pas suivre ces règles. Et comme pour la mélodie, on part d'un ensemble de notes organisé, d'une gamme caractéristique d'une tonalité, d'un mode. Les accords les plus utilisés pour une tonalité donnée sont les accords dont la fondamentale sont les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la tonalité, en particulier la triade {{Times New Roman|I}}, appelée « accord parfait » ou « accord de tonique », et l'accord de septième {{Times New Roman|V}}, appelé « septième de dominante ». Le fait d'avoir une progression d'accords qui se répète permet de structurer un morceau. Pour les morceaux courts, il participe au plaisir de l'écoute et facilite la mémorisation (par exemple le découpage couplet-refrain d'une chanson). Sur les morceaux longs, une trop grande régularité peut introduire de la lassitude, les longs morceaux sont souvent découpés en parties présentant chacune une progression régulière. Le fait d'avoir une progression régulière permet la pratique de l'improvisation : cadence en musique classique, solo en jazz et blues. ; Note : Le terme « cadence » désigne plusieurs choses différentes, et notamment en harmonie : :* une partie improvisée dans un opéra ou un concerto, sens utilisé ci-dessus ; :* une progression d'accords pour ponctuer un morceau et en particulier pour le conclure, sens utilisé dans la section suivante. === Accords peu utilisés === En mode mineur, l'accord de quinte augmentée {{Times New Roman|III<sup>+5</sup>}} est très peu utilisé. C'est un accord dissonant ; il intervient en général comme appogiature de l'accord de tonique (par exemple en ''la'' mineur : {{Times New Roman|III<sup>+5</sup>}} ''do'' - ''mi'' - ''sol''♯ → {{Times New Roman|I<sup>6</sup>}} ''do'' - ''mi'' - ''la''), ou de l'accord de dominante ({{Times New Roman|III<sup>6</sup><sub>+3</sub>}} ''mi'' - ''sol''♯ - ''do'' → {{Times New Roman|V<sup>5</sup>}} ''mi'' - ''sol''♯ - ''si''). Il peut être aussi utilisé comme préparation à l'accord de sous-dominante (enchaînement {{Times New Roman|III}} → {{Times New Roman|IV}}). Par ailleurs, il a une constitution symétrique — c'est l'empilement de deux tierces majeures — et ses renversements ont les mêmes intervalles à l'enharmonie près (quinte augmentée/sixte mineure, tierce majeure/quarte diminuée). De ce fait, un même accord est commun, par renversement et à l'enharmonie près, à trois tonalités : le premier renversement de l'accord ''do'' - ''mi'' - ''sol''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur) est enharmonique à ''mi'' - ''sol''♯ - ''si''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''do''♯ mineur) ; le second renversement est enharmonique à ''la''♭ - ''do'' - ''mi'' ({{Times New Roman|III}}<sup>e</sup> degré de ''fa'' mineur). === Accords très utilisés === Les trois accords les plus utilisés sont les accords de tonique (degré {{Times New Roman|I}}), de sous-dominante ({{Times New Roman|IV}}) et de dominante ({{Times New Roman|V}}). Ils interviennent en particulier en fin de phrase, dans les cadences. L'accord de dominante sert souvent à introduire une modulation : la modulation commence sur l'accord de dominante de la nouvelle tonalité. On note que l'accord de sous-dominante est situé une quinte juste en dessous de la tonique, les accords de dominante et de sous-dominante sont donc symétriques. En jazz, on utilise également très fréquemment l'accord de la sus-tonique (degré {{Times New Roman|II}}), souvent dans des progressions {{Times New Roman|II}} - {{Times New Roman|V}} (- {{Times New Roman|I}}). Rappelons que l'accord de sus-tonique a la fonction de sous-dominante. === Cadences et ''turnaround'' === Le terme « cadence » provient de l'italien ''cadenza'' et désigne la « chute », la fin d'un morceau ou d'une phrase musicale. On distingue deux types de cadences : * les cadences conclusive, qui créent une sensation de complétude ; * les cadences suspensives, qui crèent une sensation d'attente. ==== Cadence parfaite ==== [[Fichier:Au clair de le lune cadence parfaite.midi|thumb|''Au clair de la lune'', harmonisé avec une cadence parfaite (italienne).]] [[Fichier:Au clair de le lune mineur cadence parfaite.midi|thumb|''Idem'' mais en mode mineur harmonique.]] La cadence parfaite est l'enchaînement de l'accord de dominante suivi de l'accord parfait : {{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}}, les deux accord étant à l'état fondamental. Elle donne une impression de stabilité et est donc très souvent utilisée pour conclure un morceau. C'est une cadence conclusive. On peut aussi utiliser l'accord de septième de dominante, la dissonance introduisant une tension résolue par l'accord parfait : {{Times New Roman|V<sup>7</sup><sub>+</sub> - I<sup>5</sup>}}. Elle est souvent précédée de l'accord construit sur le IV<sup>e</sup> degré, appelé « accord de préparation », pour former la cadence italienne : {{Times New Roman|IV<sup>5</sup> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}}. Elle est également souvent précédée du second renversement de l'accord de tonique, qui est alors appelé « appoggiature de la cadence » : {{Times New Roman|I<sup>6</sup><sub>4</sub> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}} (on remarque que les accords {{Times New Roman|I}}<sup>6</sup><sub>4</sub> et {{Times New Roman|V}}<sup>5</sup> ont la basse en commun, et que l'on peut passer de l'un à l'autre par un mouvement conjoint sur les autres notes). {{clear}} ==== Demi-cadence ==== [[Fichier:Au clair de le lune demi cadence.midi|thumb|''Au clair de la lune'', harmonisé avec une demi-cadence.]] Une demi-cadence est une phrase ou un morceau se concluant sur l'accord construit sur le cinquième degré. Il provoque une sensation d'attente, de suspens. Il s'agit en général d'une succession {{Times New Roman|II - V}} ou {{Times New Roman|IV - V}}. C'est une cadence suspensive. On utilise rarement un accord de septième de dominante. {{clear}} ==== Cadence rompue ou évitée ==== La cadence rompue, ou cadence évitée, est succession d'un accord de dominante et d'un accord de sus-dominante, {{Times New Roman|V}} - {{Times New Roman|VI}}. C'est une cadence suspensive. ==== Cadence imparfaite ==== Une cadence imparfaite est une cadence {{Times New Roman|V - I}}, comme la cadence parfaite, mais dont au moins un des deux accords est dans un état renversé. ==== Cadence plagale ==== La cadence plagale — du grec ''plagios'', oblique, en biais — est la succession de l'accord construit sur le quatrième degré, suivi de l'accord parfait : {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}. Elle peut être utilisée après une cadence parfaite ({{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}} - {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}). Elle donne un caractère solennel, voire religieux — elle est parfois appelée « cadence amen » —, elle a un côté antique qui rappelle la musique modale et médiévale<ref>{{lien web |url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/la-cadence-amen-ou-comment-se-dire-adieu-7191921 |titre=La cadence « Amen » ou comment se dire adieu |auteur=Max Dozolme (MAXXI Classique) |site=France Musique |date=2025-04-25 |consulté le=2025-04-25}}.</ref>. C'est une cadence conclusive. ==== {{lang|en|Turnaround}} ==== [[Fichier:Au clair de le lune turnaround.midi|thumb|Au clair de la lune, harmonisé en style jazz : accords de 7{{e}}, anatole suivie d'un ''{{lang|en|turnaround}}'' ii-V-I.]] Le terme ''{{lang|en|turnaround}}'' signifie revirement, retournement. C'est une succession d'accords que fait la transition entre deux parties, en créant une tension-résolution. Le ''{{lang|en|turnaround}}'' le plus courant est la succession {{Times New Roman|II - V - I}}. On utilise également fréquemment l'anatole : {{Times New Roman|I - VI - II - V}}. {| class="wikitable" |+ Progressions typiques d'accords dans une tonalité majeure ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Cadence<br />parfaite<br /> {{Times New Roman|V - I}} ! scope="col" | Cadence<br />italienne<br />{{Times New Roman|IV - V - I}} ! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou IV - V}} ! scope="col" | Cadence<br />plagale<br />{{Times New Roman|IV - I}} ! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}} ! scope="col" | Anatole<br />{{Times New Roman|I - vi - ii - V}} |- |''Do'' majeur || || G - C || F - G - C || Dm - G ou F - G || F - C || Dm - G - C || C - Am - Dm - G |- |''Sol'' majeur || ''fa''♯ || D - G || C - D - G || Am - D ou C - D || C - G || Am - D - G || G - Em - Am - D |- |''Ré'' majeur || ''fa''♯, ''do''♯ || A - D || G - A - D || Em - A ou G - A || G - D || Em - A - D || D - Bm - Em - A |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || E - A || D - E - A || Bm - E ou D - E || D - A || Bm - E - A || A - F♯m - B - E |- | ''Fa'' majeur || ''si''♭ || C - F || B♭ - C - F || Gm - C ou B♭ - C || B♭ - F || Gm - C - F || F - Dm - Gm - C |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || F - B♭ || E♭ - F - B♭ || Cm - F ou E♭ - F || E♭ - B♭ || Cm - F - B♭ || B♭ - Gm - Cm - F |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || B♭ - E♭ || A♭ - B♭ - E♭ || Fm - B♭ ou A♭ - B♭ || A♭ - E♭ || Fm - B♭ - E♭ || Gm - Cm - Fm - B♭ |} {| class="wikitable" |+ Progressions typiques d'accords dans une tonalité mineure ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Cadence<br />parfaite<br />{{Times New Roman|V - i}} ! scope="col" | Cadence<br />italienne<br />{{Times New Roman|iv - V - i}} ! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou iv - V}} ! scope="col" | Cadence<br />plagale<br />{{Times New Roman|iv - i}} ! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}} ! scope="col" | Anatole<br />{{Times New Roman|i - VI - ii - V}} |- | ''La'' mineur<br />harmonique || || E - Am || Dm - E - Am || B° - E ou Dm - E || Dm - Am || B° - E - Am || Am - F - B° - E |- | ''Mi'' mineur<br />harmonique || ''fa''♯ || B - Em || Am - B - Em || F♯° - B ou Am - B || Am - Em || F♯° - B - Em || Em - C - F♯° - B |- | ''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || F♯ - Bm || Em - F♯ - Bm || C♯° - F♯ ou Em - F♯ || Em - Bm || C♯° - F♯ - Bm || Bm - G - C♯° - F♯ |- | ''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || C♯ - F♯m || Bm - C♯ - F♯m || G♯° - C♯ ou Bm - C♯ || Bm - F♯m || G♯° - C♯ - F♯m || A+ - D - G♯° - C♯ |- | ''Ré'' mineur<br />harmonique || ''si''♭ || A - Dm || Gm - A - Dm || E° - A ou Gm - A || Gm - Dm || E° - A - Dm || Dm - B♭ - E° - A |- | ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || D - Gm || Cm - D - Gm || A° - D ou Cm - D || Cm - Gm|| A° - D - Gm || Gm - E♭ - A° - D |- | ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || G - Cm || Fm - G - Cm || D° - G ou Fm - G || Fm - Dm || D° - G - Cm || Cm - A♭ - D° - G |} ==== Exemple : ''La Mer'' ==== : {{lien web | url = https://www.youtube.com/watch?v=PXQh9jTwwoA | titre = Charles Trenet - La mer (Officiel) [Live Version] | site = YouTube | auteur = Charles Trenet | consulté le = 2020-12-24 }} Le début de ''La Mer'' (Charles Trenet, 1946) est en ''do'' majeur et est harmonisé par l'anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (C - Am - Dm - G<sup>7</sup>) sur deux mesures, jouée deux fois ({{Times New Roman|1=<nowiki>|I-vi|ii-V</nowiki><sup>7</sup><nowiki>|</nowiki>}} × 2). Viennent des variations avec les progressions {{Times New Roman|I-III-vi-V<sup>7</sup>}} (C - E - Am - G<sup>7</sup>) puis la « progression ’50s » (voir plus bas) {{Times New Roman|I-vi-IV-VI<sup>7</sup>}} (C - Am - F - A<sup>7</sup>, on remarque que {{Times New Roman|IV}}/F est le relatif majeur du {{Times New Roman|ii}}/Dm de l'anatole), jouées chacune une fois sur deux mesure ; puis cette première partie se conclut par une demie cadence {{Times New Roman|ii-V<sup>7</sup>}} sur une mesure puis une dernière anatole sur trois mesures ({{Times New Roman|1=<nowiki>|I-vi|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}}). Cela constitue une première partie « A » sur douze mesures qui se termine par une demi-cadence ({{Times New Roman|ii-V<sup>7</sup>}}) qui appelle une suite. Cette partie A est jouée une deuxième fois mais la fin est modifiée pour la transition : les deux dernières mesures {{Times New Roman|<nowiki>|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}} deviennent {{Times New Roman|<nowiki>|ii-V</nowiki><sup>7</sup><nowiki>|I|</nowiki>}} (|Dm-G7|C|), cette partie « A’ » se conclut donc par une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}). Le morceau passe ensuite en tonalité de ''mi'' majeur, donc une tierce au dessus de ''do'' majeur, sur six mesures. Cette partie utilise une progression ’50s {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (E - C♯m - A - B<sup>7</sup>), qui est rappelons-le une variation de l'anatole, l'accord {{Times New Roman|ii}} (Fm) étant remplacé par son relatif majeur {{Times New Roman|IV}} (A). Cette anatole modifiée est jouée deux fois puis la partie en ''mi'' majeur se conclut par l'accord parfait {{Times New Roman|I}} joué sur deux mesures (|E|E|), on a donc, avec la mesure précédente, avec une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}). Suivent ensuite six mesures en ''sol'' majeur, donc à nouveau une tierce au dessus de ''mi'' majeur. Elle comporte une progression {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (G - Em - C - D<sup>7</sup>), donc anatole avec substitution du {{Times New Roman|ii}}/Am par son relatif majeur {{Times New Roman|VI}}/C (progression ’50s), puis une anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (G - Em - Am - D<sup>7</sup>) et deux mesure sur la tonique {{Times New Roman|I-I<sup>7</sup>}} (G - G<sup>7</sup>), formant à nouveau une cadence parfaite. La fin sur un accord de septième, dissonant, appelle une suite. Cette partie « B » de douze mesures comporte donc deux parties similaires « B1 » et « B2 » qui forment une marche harmonique (montée d'une tierce). Le morceau se conclut par une reprise de la partie « A’ » et se termine donc par une cadence parfaite. Nous avons une structure A-A’-B-A’ sur 48 mesures, proche la forme AABA étudiée plus loin. Donc ''La Mer'' est un morceau structuré autour de l'anatole avec des variations (progression ’50s, substitution du {{Times New Roman|ii}} par son relatif majeur {{Times New Roman|IV}}) et comportant une marche harmonique dans sa troisième partie. Les parties se concluent par des ''{{lang|en|turnarounds}}'' sous la forme d'une cadence parfaite ou, pour la partie A, par une demi-cadence. {| border="1" rules="rows" frame="hsides" |+ Structure de ''La Mer'' |- align="center" | | colspan="12" | ''do'' majeur | |- align="center" ! scope="row" rowspan=2 | A | colspan="2" | anatole | colspan="2" | // | colspan="2" | variation | colspan="2" | ’50s | ½ c. | colspan="3" | anatole | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii}} || <nowiki>|</nowiki> {{Times New Roman|V<sup>7</sup>}} || <nowiki>|</nowiki> |- align="center" ! scope="row" rowspan="2" | A’ | colspan="2" | anatole | colspan="2" | // | colspan="2" | variation | colspan="2" | ’50s | ½ c. | colspan="2" | anatole | c.p. | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> |- align="center" | | colspan="6" | B1 : ''mi'' majeur | colspan="6" background="lightgray" | B2 : ''sol'' majeur | |- align="center" ! scope="row" rowspan="2" | B | colspan="2" | ’50s | colspan="2" | // |colspan="2" | c.p. | colspan="2" | ’50s | colspan="2" | // |colspan="2" | c.p. | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I<sup>7</sup>}} || <nowiki>|</nowiki> |- ! scope="row" | A’ | colspan="12" | | |} === Progression blues === La musique blues est apparue dans les années 1860. Elle est en général bâtie sur une grille d'accords ''({{lang|en|changes}})'' immuable de douze mesures ''({{lang|en|twelve-bar blues}})''. C'est sur cet accompagnement qui se répète que s'ajoute la mélodie — chant et solo. Cette structure est typique du blues et se retrouve dans ses dérivés comme le rock 'n' roll. Le rythme est toujours un rythme ternaire syncopé ''({{lang|en|shuffle, swing, groove}}, ''notes inégales'')'' : la mesure est à quatre temps, mais la noire est divisée en noire-croche en triolet, ou encore triolet de croche en appuyant la première et la troisième. La mélodie se construit en général sur une gamme blues de six degrés (gamme pentatonique mineure avec une quarte augmentée), mais bien que la gamme soit mineure, l'harmonie est construite sur la gamme majeure homonyme : un blues en ''fa'' a une mélodie sur la gamme de ''fa'' mineur, mais une harmonie sur la gamme de ''fa'' majeur. La grille d'accord comporte les accords construits sur les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la gamme majeure homonyme. Les accords sont souvent des accords de septième (donc avec une tierce majeure et une septième mineure), il ne s'agit donc pas d'une harmonisation de gamme diatonique (puisque la septième est majeure sur l'accord de tonique). Par exemple, pour un blues en ''do'' : * accord parfait de do majeur, C ({{Times New Roman|I}}<sup>er</sup> degré) ; * accord parfait de fa majeur, F ({{Times New Roman|IV}}<sup>e</sup> degré) ; * accord parfait de sol majeur, G ({{Times New Roman|V}}<sup>e</sup> degré). Il existe quelques morceaux harmonisés avec des accords mineurs, comme par exemple ''As the Years Go Passing By'' d'Albert King (Duje Records, 1959). La progression blues est organisée en trois blocs de quatre mesures ayant les fonctions suivantes (voir ci-dessus ''[[#Harmonie fonctionnelle|Harmonie fonctionnelle]]'') : * quatre mesures toniques ; * quatre mesures sous-dominantes ; * quatre mesures dominantes. La forme la plus simple, que Jeff Gardner appelle « forme A », est la suivante : {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, forme A |- ! scope="row" | Tonique | width="50px" | I | width="50px" | I | width="50px" | I | width="50px" | I |- ! scope="row" | Sous-domminante | width="50px" | IV | width="50px" | IV | width="50px" | I | width="50px" | I |- ! scope="row" | Dominante | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | V |} La progression {{Times New Roman|I-V}} des deux dernières mesures forment le ''{{lang|en|turnaround}}'', la demie cadence qui lance le cycle suivant. Nous présentons ci-dessous un exemple typique de ligne de basse ''({{lang|en|walking bass}})'' pour le ''{{lang|en|turnaround}}'' d'un blues en ''la'' : [[Fichier:Turnaround classique blues en la.svg|Exemple typique de ligne de basse pour un ''turnaround'' de blues en ''la''.]] [[Fichier:Blues mi harmonie elementaire.midi|thumb|Blues en ''mi'', harmonisé de manière élémentaire avec une ''{{lang|en|walking bass}}''.]] Vous pouvez écouter ci-contre une harmonisation typique d'un blues en ''mi''. Les accords sont exécutés par une basse marchante ''({{lang|en|walking bass}})'', qui joue une arpège sur la triade avec l'ajout d'une sixte majeure et d'une septième mineure, et par une guitare qui joue un accord de puissance ''({{lang|en|power chord}})'', qui n'est composé que de la fondamentale et de la quinte juste, avec une sixte en appoggiature. La forme B s'obtient en changeant la deuxième mesure : on joue un degré {{Times New Roman|IV}} au lieu d'un degré {{Times New Roman|I}}. La progression {{Times New Roman|I-IV}} sur les deux premières mesures est appelé ''{{lang|en|quick change}}''. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, forme B |- | width="50px" | I | width="50px" | IV | width="50px" | I | width="50px" | I |- | width="50px" | IV | width="50px" | IV | width="50px" | I | width="50px" | I |- | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | V |} Par exemple, ''Sweet Home Chicago'' (Robert Johnson, 1936) est un blues en ''fa'' ; sa grille d'accords, aux variations près, suit une forme B : {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression de ''Sweet Home Chicago'' |- | width="50px" | F | width="50px" | B♭ | width="50px" | F | width="50px" | F |- | width="50px" | B♭ | width="50px" | B♭ | width="50px" | F | width="50px" | F |- | width="50px" | C7 | width="50px" | B♭7 | width="50px" | F7 | width="50px" | C7 |} : Écouter {{lien web | url =https://www.youtube.com/watch?v=dkftesK2dck | titre = Robert Johnson "Sweet Home Chicago" | auteur = Michal Angel | site = YouTube | date = 2007-12-09 | consulté le = 2020-12-17 }} Les formes C et D s'obtiennent à partir des formes A et B en changeant le dernier accord par un accord sur le degré {{Times New Roman|I}}, ce qui forme une cadence plagale. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, formes C et D |- | colspan="4" | … |- | colspan="4" | … |- | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | I |} L'harmonie peut être enrichie, notamment en jazz. Voici par exemple une grille du blues souvent utilisés en bebop. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Exemple de progression de blues bebop sur une base de forme B |- | width="60px" | I<sup>7</sup> | width="60px" | IV<sup>7</sup> | width="60px" | I<sup>7</sup> | width="60px" | V–<sup>7</sup> <nowiki>|</nowiki> I<sup>7</sup> |- | width="60px" | IV<sup>7</sup> | width="60px" | IV<sup>7</sup> | width="60px" | I<sup>7</sup> | width="60px" | VI<sup>7 ♯9 ♭13</sup> |- | width="60px" | II–<sup>7</sup> | width="60px" | V<sup>7</sup> | width="60px" | V<sup>7</sup> <nowiki>|</nowiki> IV<sup>7</sup> | width="60px" | II–<sup>7</sup> <nowiki>|</nowiki> V<sup>7</sup> |} On peut aussi trouver des blues sur huit mesures, sur seize mesures comme ''Watermelon Man'' de Herbie Hancock (album ''Takin' Off'', Blue Note, 1962) ou ''Let's Dance'' de Jim Lee (interprété par Chris Montez, Monogram, 1962) * {{lien web |url= https://www.dailymotion.com/video/x5iduwo |titre=Herbie Hancock - Watermelon Man (1962) |auteur=theUnforgettablesTv |site=Dailymotion |date=2003 |consulté le=2021-02-09 }} * {{lien web |url=https://www.youtube.com/watch?v=6JXshurYONc |titre=Let's Dance |auteur=Chris Montez |site=YouTube |date=2016-08-06 |consulté le=2021-02-09 }} À l'inverse, certains blues peuvent avoir une structure plus simple que les douze mesure ; par exemple ''Hoochie Coochie Man'' de Willie Dixon (interprété par Muddy Waters sous le titre ''Mannish Boy'', Chicago Blues, 1954) est construit sur un seul accord répété tout le long de la chanson. * {{lien web |url=https://www.dailymotion.com/video/x5iduwo |titre=Muddy Waters - Hoochie Coochie Man |auteur=Muddy Waters |site=Dailymotion |date=2012 | consulté le=2021-02-09 }} === Cadence andalouse === La cadence andalouse est une progression de quatre accords, descendant par mouvement conjoint : * en mode de ''mi'' (mode phrygien) : {{Times New Roman|IV}} - {{Times New Roman|III}} - {{Times New Roman|II}} - {{Times New Roman|I}} ;<br />par exemple en ''mi'' phrygien : Am - G - F - E ; en ''do'' phrygien : Fm - E♭ - D♭ - C ;<br />on notera que le degré {{Times New Roman|III}} est diésé dans l'accord final (ou bécarre s'il est bémol dans la tonalité) ; * en mode mineur : {{Times New Roman|I}} - {{Times New Roman|VII}} - {{Times New Roman|VI}} - {{Times New Roman|V}} ;<br />par exemple en ''la'' mineur : Am - G - F - E ; en ''do'' mineur : Cm - B♭ - A♭ - m ;<br />comme précédemment, on notera que le degré {{Times New Roman|VII}} est diésé dans l'accord final. === Progressions selon le cercle des quintes === [[Fichier:Cercle quintes degres tonalite majeure.svg|vignette|Cercle des quinte justes (parcouru dans le sens des aiguilles d'une montre) des degrés d'une tonalité majeure.]] La progression {{Times New Roman|V-I}} est la cadence parfaite, mais on peut aussi l'employer au milieu d'un morceau. Cette progression étant courte, sa répétition crée de la lassitude ; on peut la compléter par d'autres accords séparés d'une quinte juste, en suivant le « cercle des quintes » : {{Times New Roman|I-V-IX}}, la neuvième étant enharmonique de la seconde, on obtient {{Times New Roman|I-V-II}} ; en retournant la progression, on obtient le turnaround {{Times New Roman|II-V-I}}. En jazz, dans le morceau ''Topsy'' (Edgar Battle et Eddir Durham), la partie <span style="border-color: black; border-style: solid; border-width: thin;">C</span> est composée d'une progression en quintes descendantes d'accords de septième de dominante : D<sup>7</sup> - G<sup>7</sup> - C<sup>7</sup> - F<sup>7</sup> - B♭<sup>7</sup> ; les improvisatiosn sur cette partie peuvent donc se faire en modulant en ''sol'' (majeur ou mineur) - ''do'' (M/m) - ''fa'' (M/m) - ''si''♭ (M/m) - ''mi''♭ (M/m). On peut continuer de décrire le cercle des quintes : {{Times New Roman|I-V-II-VI}}, on obtient l'anatole dans le désordre ; on peut à l'inverse étendre les quintes vers la gauche, {{Times New Roman|IV-I-V-II-VI}}. En musique populaire, on trouve fréquemment une progression fondée sur les accord {{Times New Roman|I}}, {{Times New Roman|IV}}, {{Times New Roman|V}} et {{Times New Roman|VI}}, popularisée dans les années 1950. La « progression années 1950 », « progression ''{{lang|en|fifties ('50)}}'' » ''({{lang|en|'50s progression}})'' est dans l'ordre {{Times New Roman|I-VI-IV-V}}. On trouve aussi cette progression en musique classique. Si la tonalité est majeure, la triade sur la sus-dominante est mineure, les autres sont majeures, on notera donc souvent {{Times New Roman|I-vi-IV-V}}. On peut avoir des permutations circulaires (le dernier accord venant au début, ou vice-versa) : {{Times New Roman|vi-IV-V-I}}, {{Times New Roman|IV-V-I-vi}} et {{Times New Roman|V-I-vi-IV}}. {| class="wikitable" |+ Accords selon la tonalité ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" style="font-family:Times New Roman" | I ! scope="col" style="font-family:Times New Roman" | IV ! scope="col" style="font-family:Times New Roman" | V ! scope="col" style="font-family:Times New Roman" | vi |- |''Do'' majeur || || C || F || G || Am |- |''Sol'' majeur || ''fa''♯ || G || C || D || Em |- |''Ré'' majeur || ''fa''♯, ''do''♯ || D || G || A || Bm |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A || D || E || F♯m |- | ''Fa'' majeur || ''si''♭ || F || B♭ || C || Dm |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭ || E♭ || F || Gm |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭ || A♭ || B♭ || Cm |} Par exemple, en tonalité de ''do'' majeur, la progression {{Times New Roman|I-vi-IV-V}} sera C-Am-F-G. Il existe d'autres progressions utilisant ces accords mais dans un autre ordre, typiquement {{Times New Roman|I–IV–vi–V}} ou une de ses permutations circulaires : {{Times New Roman|IV–vi–V-I}}, {{Times New Roman|vi–V-I-IV}} ou {{Times New Roman|V-I-IV-vi}}. Ou dans un autre ordre. PV Nova l'illustre dans plusieurs de ses « expériences » dans la version {{Times New Roman|vi-V-IV-I}}, soit Am-G-F-C, ou encore {{Times New Roman|vi-IV-I-V}}, soit Am-F-C-G : : {{lien web | url = https://www.youtube.com/watch?v=w08LeZGbXq4 | titre = Expérience n<sup>o</sup> 6 — La Happy Pop | auteur = PV Nova | site = YouTube | date = 2011-08-20 | consulté le = 2020-12-13 }} et cela devient un gag récurrent avec son « chapeau des accords magiques qu'on nous ressort à toutes les sauces » : {{lien web | url = https://www.youtube.com/watch?v=VMY_vc4nZAU | titre = Expérience n<sup>o</sup> 14 — La Soupe dou Brasil | auteur = PV Nova | site = YouTube | date = 2012-10-03 | consulté le = 2020-12-17 }} Cette récurrence est également parodiée par le groupe The Axis of Awesome avec ses « chansons à quatre accords » ''({{lang|en|four-chords song}})'', dans une sketch où ils mêlent 47 chansons en utilisant l'ordre {{Times New Roman|I-V-vi-IV}} : : {{lien web | url = https://www.youtube.com/watch?v=oOlDewpCfZQ | titre = 4 Chords | Music Videos | The Axis Of Awesome | auteur = The Axis of Awesome | site = YouTube | date = 2011-07-20 | consulté le = 2020-12-17 }} {{boîte déroulante/début|titre=Chansons mêlées dans le sketch}} # Journey : ''Don't Stop Believing'' ; # James Blunt : ''You're Beautiful'' ; # Black Eyed Peas : ''Where Is the Love'' ; # Alphaville : ''Forever Young'' ; # Jason Mraz : ''I'm Yours'' ; # Train : ''Hey Soul Sister'' ; # The Calling : ''Wherever You Will Go'' ; # Elton John : ''Can You Feel The Love Tonight'' (''Le Roi lion'') ; # Akon : ''Don't Matter'' ; # John Denver : ''Take Me Home, Country Roads'' ; # Lady Gaga : ''Paparazzi'' ; # U2 : ''With Or Without You'' ; # The Last Goodnight : ''Pictures of You'' ; # Maroon Five : ''She Will Be Loved'' ; # The Beatles : ''Let It Be'' ; # Bob Marley : ''No Woman No Cry'' ; # Marcy Playground : ''Sex and Candy'' ; # Men At Work : ''Land Down Under'' ; # thème de ''America's Funniest Home Videos'' (équivalent des émissions ''Vidéo Gag'' et ''Drôle de vidéo'') ; # Jack Johnson : ''Taylor'' ; # Spice Girls : ''Two Become One'' ; # A Ha : ''Take On Me'' ; # Green Day : ''When I Come Around'' ; # Eagle Eye Cherry : ''Save Tonight'' ; # Toto : ''Africa'' ; # Beyonce : ''If I Were A Boy'' ; # Kelly Clarkson : ''Behind These Hazel Eyes'' ; # Jason DeRulo : ''In My Head'' ; # The Smashing Pumpkins : ''Bullet With Butterfly Wings'' ; # Joan Osborne : ''One Of Us'' ; # Avril Lavigne : ''Complicated'' ; # The Offspring : ''Self Esteem'' ; # The Offspring : ''You're Gonna Go Far Kid'' ; # Akon : ''Beautiful'' ; # Timberland featuring OneRepublic : ''Apologize'' ; # Eminem featuring Rihanna : ''Love the Way You Lie'' ; # Bon Jovi : ''It's My Life'' ; # Lady Gaga : ''Pokerface'' ; # Aqua : ''Barbie Girl'' ; # Red Hot Chili Peppers : ''Otherside'' ; # The Gregory Brothers : ''Double Rainbow'' ; # MGMT : ''Kids'' ; # Andrea Bocelli : ''Time To Say Goodbye'' ; # Robert Burns : ''Auld Lang Syne'' ; # Five for fighting : ''Superman'' ; # The Axis of Awesome : ''Birdplane'' ; # Missy Higgins : ''Scar''. {{boîte déroulante/fin}} Vous pouvez par exemple jouer les accords C-G-Am-F ({{Times New Roman|I-V-vi-IV}}) et chanter dessus ''{{lang|en|Let It Be}}'' (Paul McCartney, The Beattles, 1970) ou ''Libérée, délivrée'' (Robert Lopez, ''La Reine des neiges'', 2013). La progression {{Times New Roman|I-V-vi-IV}} est considérée comme « optimiste » tandis que sa variante {{Times New Roman|iv-IV-I-V}} est considérée comme « pessimiste ». On peut voir la progression {{Times New Roman|I-vi-IV-V}} comme une variante de l'anatole {{Times New Roman|I-vi-ii-V}}, obtenue en remplaçant l'accord de sustonique {{Times New Roman|ii}} par l'accord de sous-dominante {{Times New Roman|IV}} (son relatif majeur, et degré ayant la même fonction). ==== Exemples de progression selon le cercle des quintes en musique classique ==== [[Fichier:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.midi|vignette|Dietrich Buxtehude, Psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]] Cette progression selon la cercle des quintes, sous la forme {{Times New Roman|I-vi-IV-V}}, apparaît déjà au {{pc|xvii}}<sup>e</sup> siècle dans le psaume 42 ''Quem ad modum desiderat cervis'' (BuxVW92) de Dietrich Buxtehude (1637-1707). Le morceau est en ''fa'' majeur, la progression d'accords est donc F-Dm-B♭-C. : {{lien web | url = https://www.youtube.com/watch?v=8FmV9l1RqSg | titre = D. Buxtehude - Quemadmodum desiderat cervus, BuxWV 92 | auteur = Longobardo | site = YouTube | date = 2013-04-06 | consulté la = 2021-01-01 }} [[File:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.svg|vignette|450x450px|center|Dietrich Buxtehude, psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]] {{clear}} [[Fichier:JSBach BWV140 cantate 4 mesures.midi|vignette|J.-S. Bach, cantate BWV140, quatre premières mesures.]] On la trouve également dans l'ouverture de la cantate ''{{lang|de|Wachet auf, ruft uns die Stimme}}'' de Jean-Sébastien Bach (BWV140, 1731). Le morceau est en ''mi''♭ majeur, la progression d'accords est donc E♭-Cm-A♭<sup>6</sup>-B♭. [[Fichier:JSBach BWV140 cantate 4 mesures.svg|vignette|center|J.-S. Bach, cantate BWV140, quatre premières mesures.|alt=|517x517px]] {{clear}} [[Fichier:Mozart K310 Sonate8 mesures 45 a 49.midi|vignette|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]] La même progression est utilisée par Mozart, par exemple dans le premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778), la progression d'accords est C-Am-F-G qui correspond à la progression {{Times New Roman|III-i-VI-VII}} de ''la'' mineur, mais à la progression {{Times New Roman|I-vi-IV-V}} de la gamme relative, ''do'' majeur . [[Fichier:Mozart K310 Sonate8 mesures 45 a 49.svg|vignette|center|500px|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]] === Substitution tritonique === Un des accords les plus utilisés est donc l'accord de septième de dominante, {{Times New Roman|V<sup>7</sup><sub>+</sub>}} qui contient les degrés {{Times New Roman|V}}, {{Times New Roman|VII}}, {{Times New Roman|II}} ({{Times New Roman|IX}}) et {{Times New Roman|IV}}({{Times New Roman|XI}}) ; par exemple, en tonalité de ''do'' majeur, l'accord de ''sol'' septième (G<sup>7</sup>) contient les notes ''sol''-''si''-''ré''-''fa''. Si l'on prend l'accord dont la fondamentale est trois tons (triton) au-dessus ou en dessous — l'octave contenant six tons, on arrive sur la même note —, {{Times New Roman|♭II<sup>7</sup>}}, ici ''ré''♭ septième (D♭<sup>7</sup>), celui-ci contient les notes ''ré''♭-''fa''-''la''♭-''do''♭, cette dernière note étant l'enharmonique de ''si''. Les deux accords G<sup>7</sup> et D♭<sup>7</sup> ont donc deux notes en commun : le ''fa'' et le ''si''/''do''♭. Il est donc fréquent en jazz de substituer l'accord {{Times New Roman|V<sup>7</sup><sub>+</sub>}} par l'accord {{Times New Roman|♭II<sup>7</sup>}}. Par exemple, la progression {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|V<sup>7</sup>}}-{{Times New Roman|I<sup>&Delta;</sup>}} devient {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|♭II<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}}. C'est un procédé courant de réharmonisation (le fait de remplacer un accord par un autre dans un morceau existant). Les six substitutions possibles sont donc : C<sup>7</sup>↔F♯<sup>7</sup> - D♭<sup>7</sup>↔G<sup>7</sup> - D<sup>7</sup>↔A♭<sup>7</sup> - E♭<sup>7</sup>↔A<sup>7</sup> - E<sup>7</sup>↔B♭<sup>7</sup> - F<sup>7</sup>↔B<sup>7</sup>. [[Fichier:Übermäsiger Terzquartakkord.jpg|vignette|Exemple de cadence parfaite en ''do'' majeur avec substitution tritonique (sixte française).]] Dans l'accord D♭<sup>7</sup>, si l'on remplace le ''do''♭ par son ''si'' enharmonique, on obtient un accord de sixte augmentée : ''ré''♭-''fa''-''la''♭-''si''. Cet accord est utilisé en musique classique depuis la Renaissance ; on distingue en fait trois accords de sixte augmentée : * sixte française ''ré''♭-''fa''-''sol''-''si'' ; * sixte allemande : ''ré''♭-''fa''-''la''♭-''si'' ; * sixte italienne : ''ré''♭-''fa''-''si''. Par exemple, le ''Quintuor en ''ut'' majeur'' de Franz Schubert (1828) se termine par une cadence parfaite dont l'accord de dominante est remplacé par une sixte française ''ré''♭-''fa''-''si''-''sol''-''si'' (''ré''♭ aux violoncelles, ''fa'' à l'alto, ''si''-''sol'' aux seconds violons et ''si'' au premier violon). [[Fichier:Schubert C major Quintet ending.wav|vignette|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]] [[Fichier:Schubert C major Quintet ending.png|vignette|center|upright=2.5|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]] === Autres accords de substitution === Substituer un accord consiste à utiliser un accord provenant d'une tonalité étrangère à la tonalité en cours. À la différence d'une modulation, la substitution est très courte et ne donne pas l'impression de changer de tonalité ; on a juste un sentiment « étrange » passager. Un court passage dans une autre tonalité est également appelée « emprunt ». Nous avons déjà vu plusieurs méthodes de substitution : * utilisation d'une note étrangère : une note étrangère — note de passage, appoggiature, anticipation, retard… — crée momentanément un accord hors tonalité ; en musique classique, ceci n'est pas considéré comme un accord en propre, mais en jazz, on parle « d'accord de passage » et « d'accord suspendu » ; * utilisation d'une dominante secondaire : l'accord de dominante secondaire est hors tonalité ; le but ici est de faire une cadence parfaite, mais sur un autre degré que la tonique de la tonalité en cours ; * la substitution tritonique, vue ci-dessus, pour remplacer un accord de septième de dominante. Une dernière méthode consiste à remplacer un accord par un accord d'une gamme de même tonique, mais d'un autre mode ; on « emprunte » ''({{lang|en|borrow}})'' l'accord d'un autre mode. Par exemple, substituer un accord de la tonalité de ''do'' majeur par un accord de la tonalité de ''do'' mineur ou de ''do'' mode de ''mi'' (phrygien). Donc en ''do'' majeur, on peut remplacer un accord de ''ré'' mineur septième (D<sub>m</sub><sup>7</sup>) par un accord de ''ré'' demi-diminué (D<sup>⌀</sup>, D<sub>m</sub><sup>7&flat;5</sup>) qui est un accord appartenant à la donalité de ''la'' mineur harmonique. === Forme AABA === La forme AABA est composée de deux progressions de huit mesures, notées A et B ; cela représente trente-deux mesures au total, on parle donc souvent en anglais de la ''{{lang|en|32-bars form}}''. C'est une forme que l'on retrouve dans de nombreuses chanson de comédies musicales de Broadway comme ''Have You Met Miss Jones'' (''{{lang|en|I'd Rather Be Right}}'', 1937), ''{{lang|en|Over the Rainbow}}'' (''Le Magicien d'Oz'', Harold Harlen, 1939), ''{{lang|en|All the Things You Are}}'' (''{{lang|en|Very Warm for may}}'', 1939). Par exemple, la version de ''{{lang|en|Over the Rainbow}}'' chantée par Judy Garland est en ''la''♭ majeur et la progression d'accords est globalement : * A (couplet) : A♭-Fm | Cm-A♭ | D♭ | Cm-A♭ | D♭ | D♭-F | B♭-E♭ | A♭ * B (pont) : A♭ | B♭m | Cm | D♭ | A♭ | B♭-G | Cm-G | B♭m-E♭ soit en degrés : * A : {{Times New Roman|<nowiki>I-vi | iii-I | IV | iii-IV | IV | IV-vi | II-V | I</nowiki>}} * B : {{Times New Roman|<nowiki>I | ii | iii | IV | I | II-VII | iii-VII | ii-V</nowiki>}} Par rapport aux paroles de la chanson, on a * A : couplet 1 ''« {{lang|en|Somewhere […] lullaby}} »'' ; * A : couplet 2 ''« {{lang|en|Somewhere […] really do come true}} »'' ; * B : pont ''« {{lang|en|Someday […] you'll find me}} »'' ; * A : couplet 3 ''« {{lang|en|Somewhere […] oh why can't I?}} »'' ; : {{lien web | url = https://www.youtube.com/watch?v=1HRa4X07jdE | titre = Judy Garland - Over The Rainbow (Subtitles) | site = YouTube | auteur = Overtherainbow | consulté le = 2020-12-17 }} Une mise en œuvre de la forme AABA couramment utilisée en jazz est la forme anatole (à le pas confondre avec la succession d'accords du même nom), en anglais ''{{lang|en|rythm changes}}'' car elle s'inspire du morceau ''{{lang|en|I Got the Rythm}}'' de George Gerschwin (''Girl Crazy'', 1930) : * A : {{Times New Roman|I–vi–ii–V}} (succession d'accords « anatole ») ; * B : {{Times New Roman|III<sup>7</sup>–VI<sup>7</sup>–II<sup>7</sup>–V<sup>7</sup>}} (les fondamentales forment une succession de quartes, donc parcourent le « cercle des quintes » à l'envers). Par exemple, ''I Got the Rythm'' étant en ''ré''♭ majeur, la forme est : * A : D♭ - B♭m - E♭m - A♭ * B : F7 - B♭7 - E♭7 - A♭7 === Exemples === ==== Début du Largo de la symphonie du Nouveau Monde ==== [[File:Largo nouveau monde 5 1res mesures.svg|vignette|Partition avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]] [[File:Largo nouveau monde 5 1res mesures.midi|vignette|Fichier son avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]] Nous avons reproduit ci-contre les cinq premières mesure du deuxième mouvement Largo de la symphonie « Du Nouveau Monde » (symphonie n<sup>o</sup> 9 d'Antonín Dvořák, 1893). Cliquez sur l'image pour l'agrandir. Vous pouvez écouter cette partie jouée par un orchestre symphonique : * {{lien web |url =https://www.youtube.com/watch?v=y2Nw9r-F_yQ?t=565 |titre = Dvorak Symphony No.9 "From the New World" Karajan 1966 |site=YouTube (Seokjin Yoon) |consulté le=2020-12-11 }} (à 9 min 25), par le Berliner Philharmoniker, dirigé par Herbert von Karajan (1966) ; * {{lien web |url = https://www.youtube.com/watch?v=ASlch7R1Zvo |titre=Dvořák: Symphony №9, "From The New World" - II - Largo |site=YouTube (diesillamusicae) |consulté le=2020-12-11 }} : Wiener Philharmoniker, dirigé par Herbert von Karajan (1985). {{clear}} Cette partie fait intervenir onze instruments monodiques (ne jouant qu'une note à la fois) : des vents (trois bois, sept cuivres) et une percussion. Certains de ces instruments sont transpositeurs (les notes sur la partition ne sont pas les notes entendues). Jouées ensemble, ces onze lignes mélodiques forment des accords. Pour étudier cette partition, nous réécrivons les parties des instruments transpositeurs en ''do'' et les parties en clef d’''ut'' en clef de ''fa''. Nous regroupons les parties en clef de ''fa'' d'un côté et les parties en clef de ''sol'' d'un autre. {{boîte déroulante|Résultat|contenu=[[File:Largo nouveau monde 5 1res mesures transpositeurs en do.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde, en do.]]}} Nous pouvons alors tout regrouper sous la forme d'un système de deux portées clef de ''fa'' et clef de ''sol'', comme une partition de piano. {{boîte déroulante|Résultat|contenu= [[Fichier:Largo nouveau monde 5 1res mesures accords.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords.]] }} [[Fichier:Largo nouveau monde 5 1res mesures accords.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords.]] {{clear}} Ensuite, nous ne gardons que la basse et les notes médium. Nous changeons éventuellement certaines notes d'octave afin de n'avoir que des superpositions de tierce ou de quinte (état fondamental des accords, en faisant ressortir les notes manquantes). {{boîte déroulante|Résultat|contenu= [[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords simplifiés.]] }} [[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords simplifiés.]] Vous pouvez écouter cette partie jouée par un quintuor de cuivres (trompette, bugle, cor, trombone, tuba), donc avec des accords de cinq notes : : {{lien web |url=https://www.youtube.com/watch?v=pWfe60nbvjA |titre = Largo from The New World Symphony by Dvorak |site=YouTube (The Chamberlain Brass) |consulté le=2020-12-11 }} : The American Academy of Arts & Letters in New York City (2017). Nous allons maintenant chiffrer les accords. Pour établir la basse chiffrée, il nous faut déterminer le parcours harmonique. Pour le premier accord, les tonalités les plus simples avec un ''sol'' dièse sont ''la'' majeur et ''fa'' dièse mineur ; comme le ''mi'' est bécarre, nous retenons ''la'' majeur, il s'agit donc d'un accord de quinte sur la dominante (les accords de dominante étant très utilisés, cela nous conforte dans notre choix). Puis nous avons un ''si'' bémol, nous pouvons être en ''fa'' majeur ou en ''ré'' mineur ; nous retenons ''fa'' majeur, c'est donc le renversement d'un accord sur le degré {{Times New Roman|II}}. Dans la deuxième mesure, nous revenons en ''la'' majeur, puis, avec un ''la'' et un ''ré'' bémols, nous sommes en ''la'' bémol majeur ; nous avons donc un accord de neuvième incomplet sur la sensible, ou un accord de onzième incomplet sur la dominante. Dans la troisième mesure, nous passons en ''ré'' majeur, avec un accord de dominante. Puis, nous arrivons dans la tonalité principale, avec le renversement d'un accord de dominante sans tierce suivi d'un accord de tonique. Nous avons donc une cadence parfaite, conclusion logique d'une phrase. La progression des accords est donc : {| class="wikitable" ! scope="row" | Tonalité | ''la'' M - ''fa'' M || ''la'' M - ''la''♭ M || ''ré'' M - ''ré''♭ M || ''ré''♭ M |- ! scope="row" | Accords | {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|II}}<sup>6</sup><sub>4</sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|“V”}}<sup>9</sup><sub><s>5</s></sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|V}}<sup>+4</sup> || {{Times New Roman|I}}<sup>5</sup> |} Dans le chiffrage jazz, nous avons donc : * une triade de ''mi'' majeur, E ; * une triade de ''sol'' majeur avec un ''ré'' en basse : G/D ; * à nouveau un E ; * un accord de ''sol'' neuvième diminué incomplet, avec un ''ré'' bémol en basse : G dim<sup>9</sup>/D♭ ; * un accord de ''la'' majeur, A ; * un accord de ''la'' bémol septième avec une ''sol'' bémol à la basse : A♭<sup>7</sup>/G♭ ; * la partie se conclue par un accord parfait de ''ré''♭ majeur, D♭. Soit une progression E - G/D | E - G dim<sup>9</sup>/D♭ | A - A♭<sup>7</sup>/G♭ | D♭. [[Fichier:Largo nouveau monde 5 1res mesures accords chiffres.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde en accords simplifiés.]] {{clear}} ==== Thème de Smoke on the Water ==== Le morceau ''Smoke on the Water'' du groupe Deep Purple (album ''Machine Head'', 1972) possède un célèbre thème, un riff ''({{lang|en|rythmic figure}})'', joué à la guitare sous forme d'accords de puissance ''({{lang|en|power chords}})'', c'est-à-dire des accords sans tierce. Le morceau est en tonalité de ''sol'' mineur naturel (donc avec un ''fa''♮) avec ajout de la note bleue (''{{lang|en|blue note}}'', quinte diminuée, ''ré''♭), et les accords composant le thème sont G<sup>5</sup>, B♭<sup>5</sup>, C<sup>5</sup> et D♭<sup>5</sup>, ce dernier accord étant l'accord sur la note bleue et pouvant être considéré comme une appoggiature (indiqué entre parenthèse ci-après). On a donc ''a priori'', sur les deux premières mesures, une progression {{Times New Roman|I-III-IV}} puis {{Times New Roman|I-III-(♭V)-IV}}. Durant la majeure partie du thème, la guitare basse tient la note ''sol'' en pédale. {{note|En jazz, la qualité « <sup>5</sup> » indique que l'on n'a que la quinte (et donc pas la tierce), contrairement à la notation de basse chiffrée.}} : {{lien web | url = https://www.dailymotion.com/video/x5ili04 | titre = Deep Purple — Smoke on the Water (Live at Montreux 2006) | auteur = Deep Purple | site = Dailymotion | date = 2016 | consulté le = 2020-12-31 }} Cependant, cette progression forme une mélodie, on peut donc plus la voir comme un contrepoint, la superposition de deux voies ayant un mouvement conjoint, joué par un seul instrument, la guitare, la voie 2 étant jouée une quarte juste en dessous de la voie 1 (la quarte juste descendante étant le renversement de la quinte juste ascendante) : * voie 1 (aigu) : | ''sol'' - ''si''♭ - ''do'' | ''sol'' - ''si''♭ - (''ré''♭) - ''do'' | ; * voie 2 (grave) : | ''ré'' - ''fa'' - ''sol'' | ''ré'' - ''fa'' - (''la''♭) - ''sol'' |. En se basant sur la basse (''sol'' en pédale), nous pouvons considérer que ces deux mesures sont accompagnées d'un accord de Gm<sup>7</sup> (''sol''-''si''♭-''ré''-''fa''), chaque accord de la mélodie comprenant à chaque fois au moins une note de cet accord à l'exception de l'appogiature. {| class="wikitable" |+ Mise en évidence des notes de l'accord Gm<sup>7</sup> |- ! scope="row" | Accords | G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> |- ! scope="row" | Voie 1 | '''''sol''''' || '''''si''♭''' || ''do'' |- ! scope="row" | Voie 2 | '''''ré''''' || '''''fa''''' || '''''sol''''' |- ! scope="row" | Basse | '''''sol''''' || '''''sol''''' || '''''sol''''' |} Sur les deux mesures suivantes, la basse varie et suit les accords de la guitare avec un retard sur le dernier accord : {| class="wikitable" |+ Voies sur les mesure 3-4 du thème |- ! scope="row" | Accords | G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> || B♭<sup>5</sup> || G<sup>5</sup> |- ! scope="row" | Voie 1 | ''sol'' || ''si''♭ || ''do'' || ''si''♭ || ''sol'' |- ! scope="row" | Voie 2 | ''ré'' || ''fa'' || ''sol'' || ''fa'' || ''ré'' |- ! scope="row" | Basse | ''sol'' || ''sol'' || ''do'' || ''si''♭ || ''si''♭-''sol'' |} Le couplet de cette chanson est aussi organisé sur une progression de quatre mesures, la guitare faisant des arpèges sur les accords G<sup>5</sup> (''sol''-''ré''-''sol'') et F<sup>5</sup> (''fa''-''do''-''fa'') : : | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-F<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | soit une progression {{Times New Roman|<nowiki>| I-I | I-I | I-VII | I-I |</nowiki>}}. Nous pouvons aussi harmoniser le riff du thème sur cette progression, avec un accord F (''fa''-''la''-''do'') ; nous pouvons aussi nous rappeler que l'accord sur le degré {{Times New Roman|VII}} est plus volontiers considéré comme un accord de septième de dominante {{Times New Roman|V<sup>7</sup>}}, soit ici un accord Dm<sup>7</sup> (''ré''-''fa''-''la''-''do''). On peut donc considérer la progression harmonique sur le thème : : | Gm-Gm | Gm-Gm | Gm-F ou Dm<sup>7</sup> | Gm-Gm |. Cette analyse permet de proposer une harmonisation enrichie du morceau, tout en se rappelant qu'une des forces du morceau initial est justement la simplicité de sa structure, qui fait ressortir la virtuosité des musiciens. Nous pouvons ainsi comparer la version album à la version concert avec orchestre ou à la version latino de Pat Boone. À l'inverse, le groupe Psychostrip, dans une version grunge, a remplacé les accords par une ligne mélodique : * le thème ne contient plus qu'une seule voie (la guitare ne joue pas des accords de puissance) ; * dans les mesures 9 et 10, la deuxième guitare joue en contrepoint de type mouvement inverse, qui est en fait la voie 2 jouée en miroir ; * l'arpège sur le couplet est remplacé par une ligne mélodique en ostinato sur une gamme blues. {| class="wikitable" |+ Contrepoint sur les mesures 9 et 10 |- ! scope="row" | Guitare 1 | ''sol'' ↗ ''si''♭ ↗ ''do'' |- ! scope="row" | Guitare 2 | ''sol'' ↘ ''fa'' ↘ ''ré'' |} * {{lien web | url = https://www.dailymotion.com/video/x5ik234 | titre = Deep Purple — Smoke on the Water (In Concert with the London Symphony Orchestra, 1999) | auteur = Deep Purple | site = Dailymotion | date = 2016 | consulté le = 2020-12-31 }} * {{lien web | url = https://www.youtube.com/watch?v=MtUuNzVROIg | titre = Pat Boone — Smoke on the Water (In a Metal Mood, No More Mr. Nice Guy, 1997) | auteur = Orrore a 33 Giri | site = YouTube | date = 2019-06-24 | consulté le = 2020-12-31 }} * {{lien web | url = https://www.youtube.com/watch?v=n7zLlZ8B0Bk | titre = Smoke on the Water (Heroes, 1993) | auteur = Psychostrip | site = YouTube | date = 2018-06-20 | consulté le = 2020-12-31 }} == Accords et improvisation == Nous avons vu précédemment (chapitre ''[[../Gammes et intervalles#Modes et improvisation|Gammes et intervalles &gt; Modes et improvisation]]'') que le choix d'un mode adapté permet d'improviser sur un accord. L'harmonisation des gammes permet, en inversant le processus, d'étendre notre palette : il suffit de repérer l'accord sur une harmonisaiton de gamme, et d'utiliser cette gamme-là, dans le mode correspondant du degré de l'accord (voir ci-dessus ''[[#Harmonisation par des accords de septième|Harmonisation par des accords de septième]]''). Par exemple, nous avons vu que l'accord sur le septième degré d'une gamme majeure était un accord demi-diminué ; nous savons donc que sur un accord demi-diminué, nous pouvons improviser sur le mode correspondant au septième degré, soit le mode de ''si'' (locrien). Un accord de septième de dominante étant commun aux deux tonalités homonymes (par exemple ''fa'' majeur et ''fa'' mineur pour un ''do''<sup>7</sup><sub>+</sub> / C<sup>7</sup>), nous pouvons utiliser le mode de ''sol'' de la gamme majeure (mixolydien) ou de la gamme mineure mineure (mode phrygien dominant, ou phrygien espagnol) pour improviser. Mais l'accord de septième de dominante est aussi l'accord au début d'une grille blues ; on peut donc improviser avec une gamme blues, même si la tierce est majeure dans l'accord et mineure dans la gamme. [[Fichier:Mode improvisation accords do complet.svg]] == Autres accords courants == [[fichier:Cluster cdefg.png|vignette|Agrégat ''do - ré - mi - fa - sol''.]] Nous avons vu précédemment l'harmonisation des tonalités majeures et mineures harmoniques par des triades et des accords de septième ; certains accords étant rarement utilisés (l'accord sur le degré {{Times New Roman|III}} et, pour les tonalités mineures harmoniques, l'accord sur la tonique), certains accords étant utilisés comme des accords sur un autre degré (les accords sur la sensible étant considérés comme des accords de dominante sans fondamentale). Dans l'absolu, on peut utiliser n'importe quelle combinaison de notes, jusqu'aux agrégats, ou ''{{lang|en|clusters}}'' (mot anglais signifiant « amas », « grappe ») : un ensemble de notes contigües, séparées par des intervalles de seconde. Dans la pratique, on reste souvent sur des accords composés de superpositions de tierces, sauf dans le cas de transitions (voir la section ''[[#Notes étrangères|Notes étrangère]]''). === En musique classique === On utilise parfois des accords dont les notes ne sont pas dans la tonalité (hors modulation). Il peut s'agir d'accords de passage, de notes étrangères, par exemple utilisant un chromatisme (mouvement conjoint par demi-tons). Outre les accords de passage, les autres accords que l'on rencontre couramment en musique classique sont les accords de neuvième, et les accords de onzième et treizième sur tonique. Ces accords sont simplement obtenus en continuant à empiler les tierces. Il n'y a pas d'accord d'ordre supérieur car la quinzième est deux octaves au-dessus de la fondamentale. Comme pour les accords de septième, on distingue les accords de neuvième de dominante et les accords de neuvième d'espèce. Dans le cas de la neuvième de dominante, il y a une différence entre les tonalités majeures et mineures : l'intervalle de neuvième est respectivement majeur et mineur. Les chiffrages des renversements peuvent donc différer. Comme pour les accords de septième de dominante, on considère que les accords de septième sur le degré {{Times New Roman|VI}} sont en fait des accords de neuvième de dominante sans fondamentale. Les accords de neuvième d'espèce sont en général préparés et résolus. Préparés : la neuvième étant une note dissonante (c'est à une octave près la seconde de la fondamentale), l'accord qui précède doit contenir cette note, mais dans un accord consonant ; la neuvième est donc commune avec l'accord précédent. Résolus : la dissonance est résolue en abaissant la neuvième par un mouvement conjoint. Par exemple, en tonalité de ''do'' majeur, si l'on veut utiliser un accord de neuvième d'espèce sur la tonique ''(do - mi - sol - si - ré)'', on peut utiliser avant un accord de dominante ''(sol - si - ré)'' en préparation puis un accord parfait sur le degré {{Times New Roman|IV}} ''(fa - la - do)'' en résolution ; nous avons donc sur la voie la plus aigüe la succession ''ré'' (consonant) - ''ré'' (dissonant) - ''do'' (consonant). On rencontre également parfois des accords de onzième et de treizième. On omet en général la tierce, car elle est dissonante avec la onzième. L'accord le plus fréquemment rencontré est l'accord sur la tonique : on considère alors que c'est un accord sur la dominante que l'on a enrichi « par le bas », en ajoutant une quinte inférieure. par exemple, dans la tonalité de ''do'' majeur, l'accord ''do - sol - si - ré - fa'' est considéré comme un accord de septième de dominante sur tonique, le degré étant noté « {{Times New Roman|V}}/{{Times New Roman|I}} ». De même pour l'accord ''do - sol - si - ré - fa - la'' qui est considéré comme un accord de neuvième de dominante sur tonique. === En jazz === En jazz, on utilise fréquemment l'accord de sixte à la place de l'accord de septième majeure sur la tonique. Par exemple, en ''do'' majeur, on utilise l'accord C<sup>6</sup> ''(do - mi - sol - la)'' à la place de C<sup>Δ</sup> ''(do - mi - sol - si)''. On peut noter que C<sup>6</sup> est un renversement de Am<sup>7</sup> et pourrait donc se noter Am<sup>7</sup>/C ; cependant, le fait de le noter C<sup>6</sup> indique que l'on a bien un accord sur la tonique qui s'inscrit dans la tonalité de ''do'' majeur (et non, par exemple, de ''la'' mineur naturelle) — par rapport à l'harmonie fonctionnelle, on remarquera que Am<sup>7</sup> a une fonction tonique, l'utilisation d'un renversement de Am<sup>7</sup> à la place d'un accord de C<sup>Δ</sup> est donc logique. Les accords de neuvième, onzième et treizième sont utilisés comme accords de septième enrichis. Le chiffrage suit les règles habituelles : on ajoute un « 9 », un « 11 » ou un « 13 » au chiffrage de l'accord de septième. On utilise également des accords dits « suspendus » : ce sont des accords de transition qui sont obtenus en prenant une triade majeure ou mineure et en remplaçant la tierce par la quarte juste (cas le plus fréquent) ou la seconde majeure. Plus particulièrement, lorsque l'on parle simplement « d'accord suspendu » sans plus de précision, cela désigne l'accord de neuvième avec une quarte suspendue, noté « 9sus4 » ou simplement « sus ». == L'harmonie tonale == L'harmonie tonale est un ensemble de règle assez strictes qui s'appliquent dans la musique savante européenne, de la période baroque à la période classique classique ({{pc|xiv}}<sup>e</sup>-{{pc|xviii}}<sup>e</sup> siècle). Certaines règles sont encore largement appliquées dans divers styles musicaux actuels, y compris populaire (rock, rap…), d'autres sont au contraire ignorées (par exemple, un enchaînement de plusieurs accords de même qualité forme un mouvement parallèle, ce qui est proscrit en harmonie tonale). De nos jours, on peut voir ces règles comme des règles « de bon goût », et leur application stricte comme une manière de composer « à la manière de ». Précédemment, nous avons vu la progression des accords. Ci-après, nous abordons aussi la manière dont les notes de l'accord sont réparties entre plusieurs voix, et comment on construit chaque voix. === Concepts fondamentaux === ; Consonance : Les intervalles sont considérés comme « plus ou moins consonants » : :* consonance parfaite : unisson, quinte et octave ; :* consonance mixte (parfaite dans certains contextes, imparfaite dans d'autres) : quarte ; :* consonance imparfaite : tierce et sixte ; :* dissonance : seconde et septième. ; Degrés : Certains degrés sont considérés comme « forts », « meilleurs », ce sont les « notes tonales » : {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (sous-dominante) et {{Times New Roman|V}} (dominante). [[Fichier:Mouvements harmoniques.svg|vignette|upright=0.75|Mouvements harmoniques.]] ; Mouvements : Le mouvement décrit la manière dont les voix évoluent les unes par rapport aux autres : :# Mouvement parallèle : les voix sont séparées par un intervalle constant. :# Mouvement oblique : une voix reste constante, c'est le bourdon ; l'autre monte ou descend. :# Mouvement contraire : une voix descend, l'autre monte. :# Échange de voix : les voix échangent de note ; les mélodies se croisent mais on a toujours le même intervalle harmonique. {{clear}} === Premières règles === ; Règle du plus court chemin : Quand on passe d'un accord à l'autre, la répartition des notes se fait de sorte que chaque voix fait le plus petit mouvement possible. Notamment : si les deux accords ont des notes en commun, alors les voix concernées gardent la même note. : Les deux voix les plus importantes sont la voix aigüe — soprano — et la voix la plus grave — basse. Ces deux voix sont relativement libres : la voix de soprano a la mélodie, la voix de basse fonde l'harmonie. La règle du plus court chemin s'applique surtout aux voix intermédiaires ; si l'on a des mouvements conjoints, ou du moins de petits intervalles — c'est le sens de la règle du plus court chemin —, alors les voix sont plus faciles à interpréter. Cette règle évite également que les voix n'empiètent l'une sur l'autre (voir la règle « éviter le croisement des voix »). ; Éviter les consonances parfaites consécutives :* Lorsque deux voix sont à l'unisson ou à l'octave, elles ne doivent pas garder le même intervalle, l'effet serait trop plat. :* Lorsque deux voix sont à la quarte ou à la quinte, elles ne doivent pas garder le même intervalle, car l'effet est trop dur. : Pour éviter cela, lorsque l'on part d'un intervalle juste, on a intérêt à pratiquer un mouvement contraire aux voix qui ne gardent pas la même note, ou au moins un mouvement direct : les voix vont dans le même sens, mais l'intervalle change. : Notez que même avec le mouvement contraire, on peut avoir des consonances parfaites consécutives, par exemple si une voix fait ''do'' aigu ↗ ''sol'' aigu et l'autre ''sol'' médium ↘ ''do'' grave. : L'interdiction des consonances parfaites consécutives n'a pas été toujours appliquée, le mouvement parallèle strict a d'ailleurs été le premier procédé utilisé dans la musique religieuse au {{pc|x}}<sup>e</sup> siècle. On peut par exemple utiliser des quintes parallèles pour donner un style médiéval au morceau. On peut également utiliser des octaves parallèles sur plusieurs notes afin de créer un effet de renforcement de la mélodie. : Par ailleurs, les consonances parfaites consécutives sont acceptées lorsqu'il s'agit d'une cadence (transition entre deux parties ou bien conclusion du morceau). ; Éviter le croisement des voix : Les voix sont organisées de la plus grave à la plus aigüe. Deux voix n'étant pas à l'unisson, celle qui est plus aigüe ne doit pas devenir la plus grave et ''vice versa''. ; Soigner la partie soprano : Comme c'est celle qu'on entend le mieux, c'est en général celle qui porte la mélodie principale. On lui applique des règles spécifiques : :# Si elle chante la sensible dans un accord de dominante ({{Times New Roman|V}}), alors elle doit monter à la tonique, c'est-à-dire que la note suivante sera la tonique située un demi-ton au dessus. :# Si l'on arrive à une quinte ou une octave entre les parties basse et soprano par un mouvement direct, alors sur la partie soprano, le mouvement doit être conjoint. On doit donc arriver à cette situation par des notes voisines au soprano. ; Préférer certains accords : Les deux degrés les plus importants sont la tonique ({{Times New Roman|I}}) et la dominante ({{Times New Roman|V}}), les accords correspondants ont donc une importance particulière. : À l'inverse, l'accord de sensible ({{Times New Roman|VII}}) n'est pas considéré comme ayant une fonction harmonique forte. On le considère comme un accord de dominante affaibli. En tonalité mineure, on évite également l'accord de médiante ({{Times New Roman|III}}). : Donc on utilise en priorité les accords de : :# {{Times New Roman|I}} et {{Times New Roman|V}}. :# Puis {{Times New Roman|II}}, {{Times New Roman|IV}}, {{Times New Roman|VI}} ; et {{Times New Roman|III}} en mode majeur. :# On évite {{Times New Roman|VII}} ; et {{Times New Roman|III}} en mode mineur. ; Préférer certains enchaînements : Les enchaînements d'accord peuvent être classés par ordre de préférence. Par ordre de préférence décroissante (du « meilleur » au « moins bon ») : :# Meilleurs enchaînements : quarte ascendante ou descendante. Notons que la quarte est le renversement de la quinte, on a donc des enchaînements stables et naturels, mais avec un intervalle plus court qu'un enchaînement de quintes. :# Bons enchaînements : tierce ascendante ou descendante. Les accords consécutifs ont deux notes en commun. :# Enchaînements médiocres : seconde ascendante ou descendante. Les accords sont voisins, mais ils n'ont aucune note en commun. On les utilise de préférence en mouvement ascendant, et on utilise surtout les enchaînements {{Times New Roman|IV}}-{{Times New Roman|V}}, {{Times New Roman|V}}-{{Times New Roman|VI}} et éventuellement {{Times New Roman|I}}-{{Times New Roman|II}}. :# Les autres enchaînements sont à éviter. : On peut atténuer l'effet d'un enchaînement médiocre en plaçant le second accord sur un temps faible ou bien en passant par un accord intermédiaire. [[Fichier:Progression Vplus4 I6.svg|thumb|Résolution d'un accord de triton (quarte sensible) vers l'accord de sixte de la tonique.]] ; La septième descend par mouvement conjoint : Dans un accord de septième de dominante, la septième — qui est donc le degré {{Times New Roman|IV}} — descend par mouvement conjoint — elle est donc suivie du degré {{Times New Roman|III}}. : Corolaire : un accord {{Times New Roman|V}}<sup>+4</sup> se résout par un accord {{Times New Roman|I}}<sup>6</sup> : on a bien un enchaînement {{Times New Roman|V}} → {{Times New Roman|I}}, et la 7{{e}} (degré {{Times New Roman|IV}}), qui est la basse de l'accord {{Times New Roman|V}}<sup>+4</sup>, descend d'un degré pour donner la basse de l'accord {{Times New Roman|I}}<sup>6</sup> (degré {{Times New Roman|III}}). {{clear}} [[Fichier:Progression I64 V7plus I5.svg|thumb|Accord de sixte et de quarte cadentiel.]] ; Un accord de sixte et quarte est un accord de passage : Le second renversement d'un accord parfait est soit une appoggiature, soit un accord de passage, soit un accord de broderie. : S'il s'agit de l'accord de tonique {{Times New Roman|I}}<sup>6</sup><sub>4</sub>, c'est « accord de sixte et quarte de cadence », l'appoggiature de l'accord de dominante de la cadence parfaite. {{clear}} Mais il faut appliquer ces règles avec discernement. Par exemple, la voix la plus aigüe est celle qui s'entend le mieux, c'est donc elle qui porte la mélodie principale. Il est important qu'elle reste la plus aigüe. La voix la plus grave porte l'harmonie, elle pose les accords, il est donc également important qu'elle reste la plus grave. Ceci a deux conséquences : # Ces deux voix extrêmes peuvent avoir des intervalles mélodiques importants et donc déroger à la règle du plus court chemin : la voix aigüe parce que la mélodie prime, la voix de basse parce que la progression d'accords prime. # Les croisements des voix intermédiaires sont moins critiques. Par ailleurs, si l'on applique strictement toutes les règles « meilleurs accords, meilleurs enchaînements », on produit un effet conventionnel, stéréotypé. Il est donc important d'utiliser les solutions « moins bonnes », « médiocres » pour apporter de la variété. Ajoutons que les renversements d'accords permettent d'avoir plus de souplesse : on reste sur le même accord, mais on enrichit la mélodie sur chaque voix. Le ''Bolero'' de Maurice Ravel (1928) brise un certain nombre de ces règles. Par exemple, de la mesure 39 à la mesure 59, la harpe joue des secondes. De la mesure 149 à la mesure 165, les piccolo jouent à la sixte, dans des mouvement strictement parallèle, ce qui donne d'ailleurs une sonorité étrange. À partir de la mesure 239, de nombreux instruments jouent en mouvement parallèles (piccolos, flûtes, hautbois, cor, clarinettes et violons). === Application === [[Fichier:Harmonisation possible de frere jacques exercice.svg|vignette|Exercice : harmoniser ''Frère Jacques''.]] Harmoniser ''Frère Jacques''. Nous considérons un morceau à quatre voix : basse, ténor, alto et soprano. La soprano chante la mélodie de ''Frère Jacques''. L'exercice consiste à proposer l'écriture des trois autres voix en respectant les règles énoncées ci-dessus. Pour simplifier, nous ajoutons les contraintes suivantes : * toutes les voix chantent des blanches ; * nous nous limitons aux accords de quinte (accords de trois sons composés d'une tierce et d'une quinte) sans avoir recours à leurs renversements (accords de sixte, accords de sixte et de quarte). Les notes à gauche de la portée indiquent la tessiture (ou ambitus), l'amplitude que peut chanter la voix. {{clear}} {{boîte déroulante/début|titre=Solution possible}} [[Fichier:Harmonisation possible de frere jacques solution.svg|vignette|Harmonisation possible de ''Frère Jacques'' (solution de l'exercice).]] Il n'y a pas qu'une solution possible. Le premier accord doit contenir un ''do''. Nous sommes manifestement en tonalité de ''do'' majeur, nous proposons de commencer par l'accord parfait de ''do'' majeur, I<sup>5</sup>. Le deuxième accord doit comporter un ''ré''. Si nous utilisons l'accord de quinte de ''ré'', nous allons créer une quinte parallèle. Nous pourrions utiliser un renversement, mais nous nous imposons de chercher un autre accord. Il peut s'agir de l'accord ''si''<sup>5</sup> ''(si-ré-fa)'' ou de l'accord de ''sol''<sup>5</sup> ''(sol-si-ré)''. La dernière solution permet d'utiliser l'accord de dominante qui est un accord important de la tonalité. La règle du plus court chemin imposerait le ''sol'' grave pour la partie de basse, mais cela est proche de la limite du chanteur, nous préférons passer au ''sol'' aigu, plus facile à chanter. Nous vérifions qu'il n'y a pas de quinte parallèle : l'intervalle ascendant ''do-sol'' (basse-alto) devient ''sol-si'' (3<sup>ce</sup>), l'intervalle descendant ''do-sol'' (soprano-alto) devient ''ré-si'' (3<sup>ce</sup>). De la même manière, pour le troisième accord, nous ne pouvons pas passer à un accord de ''la''<sup>5</sup> pour éviter une quinte parallèle. Nous avons le choix entre ''do''<sup>5</sup> ''(do-mi-sol)'' et ''mi''<sup>5</sup> ''(mi-sol-si)''. Nous préférons revenir à l'accord de fondamental, solution très stable (l'enchaînement {{Times New Roman|V}}-{{Times New Roman|I}} formant une cadence parfaite). Pour le quatrième accord, nous pourrions rester sur l'accord parfait de ''do'' mais cela planterait en quelque sorte la fin du morceau puisque l'on resterait sur la cadence parfaite ; or, nous connaissons le morceau et savons qu'il n'est pas fini. Nous choisissons l'accord de ''la''<sup>5</sup> qui est une sixte ascendante ({{Times New Roman|I}}-{{Times New Roman|VI}}). Nos aurions pu répartir les voix différemment. Par exemple : * alto : ''sol''-''si''-''sol''-''do'' ; * ténor : ''mi''-''ré''-''mi''-''mi''. {{boîte déroulante/fin}} [[Fichier:Harmonisation possible de frere jacques.midi|vignette|Fichier son correspondant.]] {{clear}} == Annexe == === Accords en musique classique === Un accord est un ensemble de notes jouées simultanément. Il peut s'agir : * de notes jouées par plusieurs instruments ; * de notes jouées par un même instrument : piano, clavecin, orgue, guitare, harpe (la plupart des instruments à clavier et des instruments à corde). Pour deux notes jouées simultanément, on parle d'intervalle « harmonique » (par opposition à l'intervalle « mélodique » qui concerne les notes jouées successivement). Les notes répétées à différentes octaves ne changent pas la nature de l'accord. La musique classique considère en général des empilements de tierces ; un accord de trois notes sera constitué de deux tierces successives, un accord de quatre notes de trois tierces… Lorsque tous les intervalles sont des intervalles impairs — tierces, quintes, septièmes, neuvièmes, onzièmes, treizièmes… — alors l'accord est dit « à l'état fondamental » (ou encore « primitif » ou « direct »). La note de la plus grave est appelée « fondamentale » de l'accord. Lorsque l'accord comporte un ou des intervalles pairs, l'accord est dit « renversé » ; la note la plus grave est appelée « basse ». De manière plus générale, l'accord est dit à l'état fondamental lorsque la basse est aussi la fondamentale. On a donc un état idéal de l'accord (état canonique) — un empilement strict de tierces — et l'état réel de l'accord — l'empilement des notes réellement jouées, avec d'éventuels redoublements, omissions et inversions ; et seule la basse indique si l'accord est à l'état fondamental ou renversé. Le chiffrage dit de « basse continue » ''({{lang|it|basso continuo}})'' désigne la représentation d'un accord sous la forme d'un ou plusieurs chiffres arabes et éventuellement d'un chiffre romain. ==== Accords de trois notes ==== En musique classique, les seuls accords considérés comme parfaitement consonants, c'est-à-dire sonnant agréablement à l'oreille, sont appelés « accords parfaits ». Si l'on prend une tonalité et un mode donné, alors l'accord construit par superposition es degrés I, III et V de cette gamme porte le nom de la gamme qui l'a généré. [[fichier:Accord do majeur chiffre.svg|vignette|upright=0.5|Accord parfait de ''do'' majeur chiffré.]] Par exemple : * « l'accord parfait de ''do'' majeur » est composé des notes ''do'', ''mi'' et ''sol'' ; * « l'accord parfait de ''la'' mineur » est composé des notes ''la'', ''do'' et ''mi''. Un accord parfait majeur est donc composé, en partant de la fondamentale, d'une tierce majeure et d'une quinte juste. Un accord parfait mineur est composé d'une tierce mineure et d'une quinte juste. L'accord parfait à l'état fondamental est appelé « accord de quinte » et est simplement chiffré « 5 » pour indiquer la quinte. On peut également commencer un accord sur sa deuxième ou sa troisième note, en faisant monter celle(s) qui précède(nt) à l'octave suivante. On parle alors de « renversement d'accord » ou d'accord « renversé ». [[Fichier:Accord do majeur renversements chiffre.svg|vignette|upright=0.75|Accord parfait de ''do'' majeur et ses renversements, chiffrés.]] Par exemple, * le premier renversement de l'accord parfait de ''do'' majeur est :<br /> ''mi'', ''sol'', ''do'' ; * le second renversement de l'accord parfait de do majeur est :<br /> ''sol'', ''do'', ''mi''. Les notes conservent leur nom de « fondamentale », « tierce » et « quinte » malgré le changement d'ordre. La note la plus grave est appelée « basse ». Dans le cas du premier renversement, le deuxième note est la tierce de la basse (la note la plus grave) et la troisième note est la sixte ; le chiffrage en chiffres arabes est donc « 6 » (puisque l'on omet la tierce) et l'accord est appelé « accord de sixte ». Pour le deuxième renversement, les intervalles sont la quarte et la sixte, le chiffrage est donc « 6-4 » et l'accord est appelé « accord de sixte et de quarte ». Dans tous les cas, on chiffre le degré on considérant la fondamentale, par exemple {{Times New Roman|I}} si l'accord est construit sur la tonique de la gamme. Les autres accords de trois notes que l'on rencontre sont : * l'accord de quinte diminuée, constitué d'une tierce mineure et d'une quinte diminuée ; lorsqu'il est construit sur le septième degré d'une gamme, on considère que c'est un accord de septième de dominante sans fondamentale (voir plus bas), le degré est donc indiqué « “{{Times New Roman|V}}” » (cinq entre guillemets) et non « {{Times New Roman|VII}} » ; * l'accord de quinte augmenté : il est composé d'une tierce majeure et qu'une quinte augmentée. Dans le tableau ci-dessous, * « m » désigne un intervalle mineur ; * « M » un intervalle majeur ou le mode majeur ; * « J » un intervalle juste ; * « d » un intervalle diminué ; * « A » un intervalle augmenté ; * « mh » le mode mineur harmonique ; * « ma » le mode mineur ascendant ; * « md » le mode mineur descendant. {| class="wikitable" |+ Accords de trois notes ! scope="col" rowspan="2" | Nom ! scope="col" rowspan="2" | 3<sup>ce</sup> ! scope="col" rowspan="2" | 5<sup>te</sup> ! scope="col" rowspan="2" | État fondamental ! scope="col" rowspan="2" | 1<sup>er</sup> renversement ! scope="col" rowspan="2" | 2<sup>nd</sup> renversement ! scope="col" colspan="4"| Construit sur les degrés |- ! scope="col" | M ! scope="col" | mh ! scope="col" | ma ! scope="col" | md |- | Accord parfait<br /> majeur || M || J | accord de quinte || accord de sixte || accord de<br />sixte et de quarte | {{Times New Roman|I, IV, V}} || {{Times New Roman|V, VI}} || {{Times New Roman|IV, V}} || {{Times New Roman|III, VI, VII}} |- | Accord parfait<br /> mineur || m || J | accord de quinte || accord de sixte || accord de<br />sixte et de quarte | {{Times New Roman|II, III, VI}} || {{Times New Roman|I, IV}} || {{Times New Roman|I, II}} || {{Times New Roman|I, IV, V}} |- | Accord de<br />quinte diminuée || m || d | accord de<br />quinte diminuée || accord de<br />sixte sensible<br />sans fondamentale || accord de triton<br />sans fondamentale | {{Times New Roman|VII (“V”)}} || {{Times New Roman|II, VII (“V”)}} || {{Times New Roman|VI, VII (“V”)}} || {{Times New Roman|II}} |- | Accord de<br />quinte augmentée || M || A | accord de<br />quinte augmentée || accord de sixte<br />et de tierce sensible || accord de sixte et de quarte<br />sur sensible | &nbsp; || {{Times New Roman|III}} || {{Times New Roman|III}} || &nbsp; |} ==== Accords de quatre notes ==== Les accords de quatre notes sont des accord composés de trois tierces superposées. La dernière note étant le septième degré de la gamme, on parle aussi d'accords de septième. Ces accords sont dissonants : ils contiennent un intervalle de septième (soit une octave montante suivie d'une seconde descendante). Ils laissent donc une impression de « tension ». Il existe sept différents types d'accords, ou « espèces ». Citons l'accord de septième de dominante, l'accord de septième mineure et l'accord de septième majeure. ===== L'accord de septième de dominante ===== [[Fichier:Accord 7e dominante do majeur renversements chiffre.svg|vignette|Accord de septième de dominante de ''do'' majeur et ses renversements, chiffrés.]] L'accord de septième de dominante est l'empilement de trois tierces à partir de la dominante de la gamme, c'est-à-dire du {{Times New Roman|V}}<sup>e</sup> degré. Par exemple, l'accord de septième de dominante de ''do'' majeur est l'accord ''sol''-''si''-''ré''-''fa'', et l'accord de septième de dominante de ''la'' mineur est ''mi''-''sol''♯-''si''-''ré''. L'accord de septième de dominante dont la fondamentale est ''do'' (''do''-''mi''-''sol''-''si''♭) appartient à la gamme de ''fa'' majeur. Que le mode soit majeur ou mineur, il est composé d'une tierce majeure, d'une quinte juste et d'une septième mineure (c'est un accord parfait majeur auquel on ajoute une septième mineure). C'est de loin l'accord de septième le plus utilisé ; il apparaît au {{pc|xvii}}<sup>e</sup> en musique classique. Dans son état fondamental, son chiffrage est {{Times New Roman|V 7/+}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}). Le signe plus indique la sensible. Son premier renversement est appelé « accord de quinte diminuée et sixte » et est noté {{Times New Roman|V 6/<s>5</s>}} (ou {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}). Son deuxième renversement est appelé « accord de sixte sensible », puisque la sixte de l'accord est la sensible de la gamme, et est noté {{Times New Roman|V +6}} (ou {{Times New Roman|V<sup>+6</sup>}}). Son troisième renversement est appelé « accord de quarte sensible » et est noté {{Times New Roman|V +4}} (ou {{Times New Roman|V<sup>+4</sup>}}). [[Fichier:Accord 7e dominante sans fondamentale do majeur renversements chiffre.svg|vignette|Accord de septième de dominante sans fondamentale de ''do'' majeur et ses renversements, chiffrés.]] On utilise aussi l'accord de septième de dominante sans fondamentale ; c'est alors un accord de trois notes. Dans son état fondamental, c'est un « accord de quinte diminuée » placé sur le {{Times New Roman|VII}}<sup>e</sup> degré (mais c'est bien un accord construit sur le {{Times New Roman|V}}<sup>e</sup> degré), noté {{Times New Roman|“V” <s>5</s>}} (ou {{Times New Roman|“V”<sup><s>5</s></sup>}}). Notez les guillemets qui indiquent que la fondamentale V est absente. Dans son premier renversement, c'est un « accord de sixte sensible sans fondamentale » noté {{Times New Roman|“V” +6/3}} (ou {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}}). Dans son second renversement, c'est un « accord de triton sans fondamentale » (puisque le premier intervalle est une quarte augmentée qui comporte trois tons) noté {{Times New Roman|“V” 6/+4}} (ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}). Notons qu'un accord de septième de dominante n'a pas toujours la dominante pour fondamentale : tout accord composé d'une tierce majeure, d'une quinte juste et d'une septième mineure est un accord de septième de dominante et est chiffré {{Times New Roman|<sup>7</sup><sub>+</sub>}}, quel que soit le degré sur lequel il est bâti (certaines notes peuvent avoir une altération accidentelle). ===== Les accords de septième d'espèce ===== Les autres accords de septièmes sont dits « d'espèce ». L'accord de septième mineure est l'accord de septième formé sur la fondamentale d'une gamme mineure ''naturelle''. Par exemple, l'accord de septième mineure de ''la'' est ''la''-''do''-''mi''-''sol''. Il est composé d'une tierce mineure, d'une quinte juste et d'une septième mineure (c'est un accord parfait mineur auquel on ajoute une septième mineure). L'accord de septième majeure est l'accord de septième formé sur la fondamentale d'une gamme majeure. Par exemple, L'accord de septième majeure de ''do'' est ''do''-''mi''-''sol''-''si''. Il est composé d'une tierce majeure, d'une quinte juste et d'une septième majeure (c'est un accord parfait majeur auquel on ajoute une septième majeure). ==== Utilisation du chiffrage ==== Le chiffrage est utilisé de deux manières. La première manière, c'est la notation de la basse continue. La basse continue est une technique d'improvisation utilisée dans le baroque pour l'accompagnement d'instruments solistes. Sur la partition, on indique en général la note de basse de l'accord et le chiffrage en chiffres arabes. La seconde manière, c'est pour l'analyse d'une partition. Le fait de chiffrer les accords permet de mieux en comprendre la structure. De manière générale, on peut retenir que : * le chiffrage « 5 » indique un accord parfait, superposition d'une tierce (majeure ou mineure) et d'une quinte juste ; * le chiffrage « 6 » indique le premier renversement d'un accord parfait ; * le chiffrage « 6/4 » indique le second renversement d'un accord parfait ; * chiffrage « 7/+ » indique un accord de septième de dominante ; * le signe « + » indique en général que la note de l'intervalle est la sensible ; * un intervalle barré désigne un intervalle diminué. [[fichier:Accords gamme do majeur la mineur.svg|class=transparent| center | Principaux accords construits sur les gammes de ''do'' majeur et de ''la'' mineur harmonique.]] === Notation « jazz » === En jazz et de manière générale en musique rock et populaire, la base d'un accord est la triade composée d'une tierce (majeure ou mineure) et d'une quinte juste. Pour désigner un accord, on utilise la note fondamentale, éventuellement désigné par une lettre dans le système anglo-saxon (A pour ''la'' etc.), suivi d'une qualité (comme « m », « + »…). Les renversements ne sont pas notés de manière particulière, ils sont notés comme les formes fondamentales. Dans les deux tableaux suivants, la fondamentale est notée X (remplace le C pour un accord de ''do'', le D pour un accord de ''ré''…). La construction des accords est décrite par la suite. [[Fichier:Arbre accords triades 5d5J5A.svg|vignette|upright=1.5|Formation des triades présentée sous forme d'arbre.]] {| class="wikitable" |+ Notation des principales triades |- | ! scope="col" | Tierce<br />mineure (3m) ! scope="col" | Tierce<br />majeure (3M) |- ! scope="row" | Quinte diminuée (5d) | X<sup>o</sup>, Xm<sup>♭5</sup>, X–<sup>♭5</sup> || |- ! scope="row" | Quinte juste (5J) | Xm, X– || X |- ! scope="row" | Quinte augmentée (5A) | || X+, X<sup>♯5</sup> |} [[Fichier:Triades do.svg|class=transparent|center|Triades de do.]] {| class="wikitable" |+ Notation des principaux accords de septième |- | colspan="2" | ! scope="col" | Tierce<br />mineure (3m) ! scope="col" | Tierce<br />majeure (3M) |- ! scope="row" rowspan="2" | Quinte<br />diminuée (5d) ! scope="row" | Septième diminuée (7d) | X<sup>o7</sup> || |- ! scope="row" | Septième mineure (7m) | Xm<sup>7(♭5)</sup>, X–<sup>7(♭5)</sup>, X<sup>Ø</sup> || |- ! scope="row" rowspan="3" | Quinte<br />juste (5J) ! scope="row" | Sixte majeure (6M) | Xm<sup>6</sup> || X<sup>6</sup> |- ! scope="row" | Septième mineure (7m) | Xm<sup>7</sup>, X–<sup>7</sup> || X<sup>7</sup> |- ! scope="row" | Septième majeure (7M) | Xm<sup>maj7</sup>, X–<sup>maj7</sup>, Xm<sup>Δ</sup>, X–<sup>Δ</sup> || X<sup>maj7</sup>, X<sup>Δ</sup> |- ! scope="row" rowspan="2" | Quinte<br />augmentée (5A) ! scope="row" | Septième mineure (7m) | || X+<sup>7</sup> |- ! scope="row" | Septième majeure (7M) | || X+<sup>maj7</sup> |} [[Fichier:Arbre accords septieme.svg|class=transparent|center|Formation des accords de septième présentée sous forme d'arbre.]] &nbsp; &nbsp; [[Fichier:Accords do septieme.svg|class=transparent|center|Accord de do septième.]] On notera que l'intervalle de sixte majeure est l'enharmonique de celui de septième diminuée (6M = 7d). [[File:Principaux accords do.svg|class=transparent|center|Principaux accords de do.]] ==== Triades ==== ; Accords fondés sur une tierce majeure * accord parfait majeur : pas de notation *: p. ex. « ''do'' » ou « C » pour l'accord parfait de ''do'' majeur (''do'' - ''mi'' - ''sol'') ; Accords fondés sur une tierce mineure * accord parfait mineur : « m », « min » ou « – » *: « ''do'' m », « ''do'' – », « Cm », « C– »… pour l'accord parfait de ''do'' mineur (''do'' - ''mi''♭ - ''sol'') ==== Triades modifiées ==== ; Accords fondés sur une tierce majeure * accord augmenté (la quinte est augmentée) : aug, +, ♯5 *: « ''do'' aug », « ''do'' + », « ''do''<sup>♯5</sup> » « Caug », « C+ » ou « C<sup>♯5</sup> » pour l'accord de ''do'' augmenté (''do'' - ''mi'' - ''sol''♯) : L'accord augmenté est un empilement de tierces majeures. Ainsi, un accord augmenté a deux notes communes avec deux autres accords augmentés : C+ (''do'' - ''mi'' - ''sol''♯) a deux notes communes avec A♭+ (''la''♭ - ''do'' - ''mi'') et avec E+ (''mi'' - ''sol''♯ - ''si''♯) ; et on remarque que ces trois accords sont en fait enharmoniques (avec les enharmonies ''la''♭ = ''sol''♯ et ''si''♯ = ''do''). En effet, l'octave comporte six tons (sous la forme de cinq tons et deux demi-tons), et une tierce majeure comporte deux tons, on arrive donc à l'octave en ajoutant une tierce majeure à la dernière note de l'accord. ; Accords fondés sur une tierce mineure * accord diminué (la quinte est diminuée) : dim, o, ♭5 *: « ''do'' dim », « ''do''<sup>o</sup> », « ''do''<sup>♭5</sup> », « Cdim », « C<sup>o</sup> » ou « C<sup>♭5</sup> » pour l'accord de ''do'' diminuné (''do'' - ''mi''♭ - ''sol''♭) : On remarque que la quinte diminuée est l'enharmonique de la quarte augmentée et est l'intervalle appelé « triton » (car composé de trois tons). ; Accords fondés sur une tierce majeure ou mineure * accord suspendu de seconde : la tierce est remplacée par une seconde majeure : sus2 *: « ''do''<sup>sus2</sup> » ou « C<sup>sus2</sup> » pour l'accord de ''do'' majeur suspendu de seconde (''do''-''ré''-''sol'') * accord suspendu de quarte : la tierce est remplacée par une quarte juste : sus4 *: « ''do''<sup>sus4</sup> » ou « C<sup>sus4</sup> » pour l'accord de ''do'' majeur suspendu de quarte (''do''-''fa''-''sol'') ==== Triades appauvries ==== ; Accords fondés sur une tierce majeure ou mineure * accord de puissance : la tierce est omise, l'accord n'est constitué que de la fondamentale et de la quinte juste : 5 *: « ''do''<sup>5</sup> », « C<sup>5</sup> » pour l'accord de puissance de ''do'' (''do'' - ''la'') {{note|Très utilisé dans les musiques rock, hard rock et heavy metal, il est souvent joué renversé (''la'' - ''do'') ou bien avec l'ajout de l'octave (''do'' - ''la'' - ''do'').}} ==== Triades enrichies ==== ; Accords fondés sur une tierce majeure * accord de septième (la 7<sup>e</sup> est mineure) : 7 *: « ''do''<sup>7</sup> », « C<sup>7</sup> » pour l'accord de ''do'' septième, appelé « accord de septième de dominante de ''fa'' majeur » en musique classique (''do'' - ''mi'' - ''sol'' - ''si''♭) * accord de septième majeure : Δ, 7M ou maj7 *: « ''do'' <sup>Δ</sup> », « ''do'' <sup>maj7</sup> », « C<sup>Δ</sup> », « C<sup>7M</sup> »… pour l'accord de ''do'' septième majeure (''do'' - ''mi'' - ''sol'' - ''si'') ; Accords fondés sur une tierce mineure * accord de mineur septième (la tierce et la 7<sup>e</sup> sont mineures) : m7, min7 ou –7 *: « ''do'' m<sup>7</sup> », « ''do'' –<sup>7</sup> », « Cm<sup>7</sup> », « C–<sup>7</sup> »… pour l'accord de ''do'' mineur septième, appelé « accord de septième de dominante de ''fa'' mineur » en musique classique (''do'' - ''mi''♭ - ''sol'' - ''si''♭) * accord mineure septième majeure : m7M, m7maj, mΔ, –7M, –7maj, –Δ *: « ''do'' m<sup>7M</sup> », « ''do'' m<sup>maj7</sup> », « ''do'' –<sup>Δ</sup> », « Cm<sup>7M</sup> », « Cm<sup>maj7</sup> », « C–<sup>Δ</sup> »… pour l'accord de ''do'' mineur septième majeure (''do'' - ''mi''♭ - ''sol'' - ''si'') * accord de septième diminué (la quinte et la septième sont diminuée) : dim 7 ou o7 *: « ''do'' dim<sup>7</sup> », « ''do''<sup>o7</sup> », « Cdim<sup>7</sup> » ou « C<sup>o7</sup> » pour l'accord de ''do'' septième diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si''♭) * accord demi-diminué (seule la quinte est diminuée, la septième est mineure) : Ø ou –7(♭5) *: « ''do''<sup>Ø</sup> », « ''do''<sup>7(♭5)</sup> », « C<sup>Ø</sup> » ou « C<sup>7♭5</sup> » pour l'accord de ''do'' demi-diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si'') === Construction pythagoricienne des accords === Nous avons vu au débuts que lorsque l'on joue deux notes en même temps, leurs vibrations se superposent. Certaines superpositions créent un phénomène de battement désagréable, c'est le cas des secondes. Dans le cas d'une tierce majeure, les fréquences des notes quadruple et quintuple d'une même base : les fréquences s'écrivent 4׃<sub>0</sub> et 5׃<sub>0</sub>. Cette superposition de vibrations est agréable à l'oreille. Nous avons également vu que dans le cas d'une quinte juste, les fréquences sont le double et le triple d'une même base, ou encore le quadruple et sextuple si l'on considère la moitié de cette base. Ainsi, dans un accord parfait majeur, les fréquences des fondamentales des notes sont dans un rapport 4, 5, 6. De même, dans le cas d'un accord parfait mineur, les proportions sont de 1/6, 1/5 et 1/4. {{voir|[[../Caractéristiques_et_notation_des_sons_musicaux#Construction_pythagoricienne_et_gamme_de_sept_tons|Caractéristiques et notation des sons musicaux &gt; Construction pythagoricienne et gamme de sept tons]]}} === Un peu de physique : interférences === Les sons sont des vibrations. Lorsque l'on émet deux sons ou plus simultanément, les vibrations se superposent, on parle en physique « d'interférences ». Le modèle le plus simple pour décrire une vibration est la [[w:fr:Fonction sinus|fonction sinus]] : la pression de l'air P varie en fonction du temps ''t'' (en secondes, s), et l'on a pour un son « pur » : : P(''t'') ≈ sin(2π⋅ƒ⋅''t'') où ƒ est la fréquence (en hertz, Hz) du son. Si l'on émet deux sons de fréquence respective ƒ<sub>1</sub> et ƒ<sub>2</sub>, alors la pression vaut : : P(''t'') ≈ sin(2π⋅ƒ<sub>1</sub>⋅''t'') + sin(2π⋅ƒ<sub>2</sub>⋅''t''). Nous avons ici une [[w:fr:Identité trigonométrique#Transformation_de_sommes_en_produits,_ou_antilinéarisation|identité trigonométrique]] dite « antilinéarisation » : : <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{f_1 + f_2}{2}t \right ) \cdot \sin \left ( 2\pi \frac{f_1 - f_2}{2}t \right ).</math> On peut étudier simplement deux situations simples. [[Fichier:Battements interferentiels.png|vignette|Deux sons de fréquences proches créent des battements : la superposition d'une fréquence et d'une enveloppe.]] La première, c'est quand les fréquences ƒ<sub>1</sub> et ƒ<sub>2</sub> sont très proches. Alors, la moyenne (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2 est très proche de ƒ<sub>1</sub> et ƒ<sub>2</sub> ; et la demie différence (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2 est très proche de zéro. On a donc une enveloppe de fréquence très faible, (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2, dans laquelle s'inscrit un son de fréquence moyenne, (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2. C'est cette enveloppe de fréquence très faible qui crée les battements, désagréables à l'oreille. Sur l'image ci-contre, le premier trait rouge montre un instant où les vibrations sont opposées ; elles s'annulent, le son s'éteint. Le second trait rouge montre un instant où les vibrations sont en phase : elle s'ajoutent, le son est au plus fort. {{clear}} La seconde, c'est lorsque les deux fréquences sont des multiples entiers d'une même fréquence fondamentale ƒ<sub>0</sub> : ƒ<sub>1</sub> = ''n''<sub>1</sub>⋅ƒ<sub>0</sub> et ƒ<sub>0</sub> = ''n''<sub>0</sub>⋅ƒ<sub>0</sub>. On a alors : : <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{n_1 + n_2}{2}f_0 \cdot t \right ) \cdot \sin \left ( 2\pi \frac{n_1 - n_2}{2}f_0 \cdot t \right ).</math> On multiplie donc deux fonctions qui ont des fréquences multiples de ƒ<sub>0</sub>. La différence minimale entre ''n''<sub>1</sub> et ''n''<sub>2</sub> vaut 1 ; on a donc une enveloppe dont la fréquence est au minimum la moitié de ƒ<sub>0</sub>, c'est-à-dire un son une octave en dessous de ƒ<sub>0</sub>. Donc, cette enveloppe ne crée pas d'effet de battement, ou plutôt, le battement est trop rapide pour être perçu comme tel. Dans cette enveloppe, on a une fonction sinus dont la fréquence est également un multiple de ƒ<sub>0</sub> ; l'enveloppe et la fonction qui y est inscrite ont donc de nombreux « points communs », d'où l'effet harmonieux. === Le tonnetz === [[File:Speculum musicae.png|thumb|right|225px|Euler, ''De harmoniæ veris principiis'', 1774, p. 350.]] En allemand, le terme ''Tonnetz'' (se prononce « tône-netz ») signifie « réseau tonal ». C'est une représentation graphique des notes qui a été imaginée par [[w:Leonhard Euler|Leonhard Euler]] en 1739. Cette représentation graphique peut aider à la mémorisation de certains concepts de l'harmonie. Cependant, son application est très limitée : elle ne concerne que l'intonation juste d'une part, et que les accords parfait des tonalités majeures et mineures naturelles d'autre part. La représentation contenant les douze notes de la musique savante occidentale, on peut bien sûr représenter d'autres objets, comme les accords de septième ou les accords diminués, mais la représentation graphique est alors compliquée et perd son intérêt pédagogique. On part d'une note, par exemple le ''do''. Si on progresse vers la droite, on monte d'une quinte juste, donc ''sol'' ; vers la gauche, on descend d'une quinte juste, donc ''fa''. Si on va vers le bas, on monte d'une tierce majeure, donc ''mi'' ; si on va vers le haut, on descend d'une tierce majeure, donc ''la''♭ ou ''sol''♯ fa — do — sol — ré | | | | la — mi — si — fa♯ | | | | do♯ — sol♯ — ré♯ — si♭ La figure forme donc un filet, un réseau. On voit que ce réseau « boucle » : si on descend depuis le ''do''♯, on monte d'une tierce majeure, on obtient un ''mi''♯ qui est l'enharmonique du ''fa'' qui est en haut de la colonne. Si on va vers la droite à partir du ''ré'', on obtient le ''la'' qui est au début de la ligne suivante. Si on ajoute des diagonales allant vers la droite et le haut « / », on met en évidence des tierces mineures : ''la'' - ''do'', ''mi'' - ''sol'', ''si'' - ''ré'', ''do''♯ - ''mi''… fa — do — sol — ré | / | / | / | la — mi — si — fa♯ | / | / | / | do♯ — sol♯ — ré♯ — si♭ Donc les liens représentent : * | : tierce majeure ; * — : quinte juste ; * / : tierce mineure. [[Fichier:Tonnetz carre accords fr.svg|thumb|Tonnetz avec les accords parfaits. Les notes sont en notation italienne et les accords en notation jazz.]] On met ainsi en évidence des triangles dont un côté est une quinte juste, un côté une tierce majeure et un côté une tierce mineure ; c'est-à-dire que les notes aux sommets du triangle forment un accord parfait majeur (par exemple ''do'' - ''mi'' - ''sol'') : <div style="font-family:courier; background-color:#fafafa"> fa&nbsp;&nbsp;—&nbsp;'''do&nbsp;&nbsp;&nbsp;—&nbsp;sol'''&nbsp;—&nbsp;ré<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;'''|&nbsp;&nbsp;&nbsp;/'''&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;|<br /> la&nbsp;&nbsp;—&nbsp;'''mi'''&nbsp;&nbsp;&nbsp;—&nbsp;si&nbsp;&nbsp;—&nbsp;fa♯<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;/&nbsp;&nbsp;|<br /> do♯&nbsp;—&nbsp;sol♯&nbsp;—&nbsp;ré♯&nbsp;—&nbsp;si♭ </div> ou un accord parfait mineur (''la'' - ''do'' - ''mi''). <div style="font-family:courier; background-color:#fafafa"> fa&nbsp;&nbsp;—&nbsp;'''do'''&nbsp;&nbsp;&nbsp;—&nbsp;sol&nbsp;—&nbsp;ré<br /> |&nbsp;&nbsp;'''/&nbsp;&nbsp;|'''&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;|<br /> '''la&nbsp;&nbsp;—&nbsp;mi'''&nbsp;&nbsp;&nbsp;—&nbsp;si&nbsp;&nbsp;—&nbsp;fa♯<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;/&nbsp;&nbsp;|<br /> do♯&nbsp;—&nbsp;sol♯&nbsp;—&nbsp;ré♯&nbsp;—&nbsp;si♭ </div> Un triangle représente donc un accord, et un sommet représente une note. Si on passe d'un triangle à un triangle voisin, alors on passe d'un accord à un autre accord, les deux accords ayant deux notes en commun. Ceci illustre la notion de « plus court chemin » en harmonie : si on passe d'un accord à un autre en gardant un côté commun, alors on a un mouvement conjoint sur une seule des trois voix. Par rapport à l'harmonie fonctionnelle : les accords sont contigus à leur fonction, par exemple en ''do'' majeur : * fonction de tonique ({{Times New Roman|I}}) : C, A– et E– sont contigus ; * fonction de sous-dominante ({{Times New Roman|IV}}) : F et D– sont contigus ; * fonction de dominante ({{Times New Roman|V}}) : G et B<sup>o</sup> sont contigus. On notera que les triangles d'un schéma ''tonnetz'' ne représentent que des accords parfaits. Pour représenter un accord de quinte diminuée (''si'' - ''ré'' - ''fa'') ou les accords de septième, en particulier l'accord de septième de dominante, il faut étendre le ''tonnetz'' et l'on obtient des figures différentes. Par ailleurs, il est adapté à ce que l'on appelle « l'intonation juste », puisque tous les intervalles sont idéaux. [[Fichier:Tonnetz carre accords etendu fr.svg|vignette|Tonnetz étendu.]] [[Fichier:Tonnetz carre do majeur accords fr.svg|vignette|Tonnetz de la tonalité de ''do'' majeur. La représentation de l'accord de quinte diminuée sur ''si'' (B<sup>o</sup>) est une ligne et non un triangle.]] [[Fichier:Tonnetz carre do mineur accords fr.svg|vignette|Tonnetz des tonalités de ''do'' mineur naturel (haut) et ''do'' mineur harmonique (bas).]] Si l'on étend un peu le réseau : ré♭ — la♭ — mi♭ — si♭ — fa | / | / | / | / | fa — do — sol — ré — la | / | / | / | / | la — mi — si — fa♯ — do♯ | / | / | / | / | do♯ — sol♯ — ré♯ — la♯ — mi♯ | / | / | / | / | mi♯ — do — sol — ré — la on peut donc trouver des chemins permettant de représenter les accords de septième de dominante (par exemple en ''do'' majeur, G<sup>7</sup>) fa / sol — ré | / si et des accords de quinte diminuée (en ''do'' majeur : B<sup>o</sup>) fa / ré / si Une gamme majeure ou mineure naturelle peut se représenter par un trapèze rectangle : ''do'' majeur fa — do — sol — ré | / la — mi — si et ''do'' mineur la♭ — mi♭ — si♭ / | fa — do — sol — ré En revanche, la représentation d'une tonalité nécessite d'étendre le réseau afin de pouvoir faire figurer tous les accords, deux notes sont représentées deux fois. La représentation des tonalités mineures harmoniques prend une forme biscornue, ce qui nuit à l'intérêt pédagogique de la représentation. [[Fichier:Neo-Riemannian Tonnetz.svg|vignette|upright=2|Tonnetz avec des triangles équilatéraux.]] On peut réorganiser le schéma en décalant les lignes, afin d'avoir des triangles équilatéraux. Sur la figure ci-contre (en notation anglo-saxonne) : * si on monte en allant vers la droite « / », on a une tierce mineure ; * si on descend en allant vers la droite « \ », on a une tierce majeure ; * les liens horizontaux « — » représentent toujours des quintes justes * les triangles pointe en haut sont des accords parfaits mineurs ; * les triangles pointe en bas sont des accords parfaits majeurs. On a alors les accords de septième de dominante F / G — D \ / B et de quinte diminuée F / D / B les tonalités majeures F — C — G — D \ / A — E — B et les tonalités mineures naturelles A♭ — E♭ — B♭ / \ F — C — G — D == Notes et références == {{références}} == Voir aussi == === Bibliographie === * {{ouvrage |prénom1=Francesco |nom1=Gasparini |titre=L'Armonico Pratico al Cimbalo |éditeur=Giusseppe Antonio Silvani |lieu=Bologne |année=1772 |lang=it |url=https://imslp.org/wiki/L'Armonico_Pratico_al_Cimbalo_(Gasparini%2C_Francesco)}} === Liens externes === {{wikipédia|Consonance (harmonie tonale)}} {{wikipédia|Disposition de l'accord}} {{wikisource|Petit Manuel d’harmonie}} * {{lien web | url = https://www.apprendrelesolfege.com/chiffrage-d-accords | titre = Chiffrage d'accords (classique) | site = Apprendrelesolfege.com | consulté le = 2020-12-03 }} * {{lien web | url = https://www.coursd-harmonie.fr/introduction/introduction2_le_chiffrage_d_accords.php | titre = Introduction II : Le chiffrage d'accords | site = Cours d'harmonie.fr | consulté le = 2021-12-14 }} * {{lien web | url=https://www.coursd-harmonie.fr/ | titre = Cours d'harmonie en ligne | auteur = Jean-Baptiste Voinet | site=coursd-harmonie.fr | consulté le = 2021-12-20 }} * {{lien web | url=http://e-harmonie.e-monsite.com/ | titre = Cours d'harmonie classique en ligne | auteur = Olivier Miquel | site=e-harmonie | consulté le = 2021-12-24 }} * {{lien web | url=https://fr.audiofanzine.com/theorie-musicale/editorial/dossiers/les-gammes-et-les-modes.html | titre = Les bases de l’harmonie | site = AudioFanzine | date = 2013-07-23 | consulté le = 2024-01-12 }} * {{lien web | url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/l-accord-de-tristan-dechiffre-et-desacralise-2481605 | titre = L’Accord de Tristan déchiffré et désacralisé ! | site = France Musique | date = 2025-10-22 | consulté le = 2025-10-23 }} * {{lien web | url=https://www.youtube.com/watch?v=X62SAnyfQYw | titre = Qu'est-ce que la basse continue dans la musique baroque ? | auteur = Clément Rochefort | site = YouTube/France Musique | date = 2022-05-02 | consulté le = 2026-08-04 }} ---- ''[[../Mélodie|Mélodie]]'' &lt; [[../|↑]] &gt; ''[[../Représentation musicale|Représentation musicale]]'' [[Catégorie:Formation musicale (livre)|Harmonie]] cs8t2oriwahspl50munn1lbxo96p8oo 773907 773906 2026-10-02T08:23:02Z Cdang 1202 /* Contrepoint */ voc. 773907 wikitext text/x-wiki {{Bases de solfège}} <span style="font-size:25px;">6. Harmonie</span> L'harmonie désigne les notes jouées en même temps, soit plusieurs instruments jouant chacun une note, soit un instrument jouant un accord (instrument dit polyphonique). == Première approche == L'exemple le plus simple d'harmonie est sans doute la chanson en canon : c'est un chant polyphonique, c'est-à-dire à plusieurs voix, chaque voix chantant la même chose en décalé. Prenons par exemple ''Vent frais, vent du matin'' (la version originale est ''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609) : [[Fichier:Vent frais vent du matin.svg|class=transparent|center|Partition de ''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]] [[Fichier:Vent frais vent du matin.midi|vignette|''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]] nous voyons que les voix se superposent de manière « harmonieuse ». Les notes de chaque voix se correspondent point par point (avec un retard), c'est donc un type d'harmonie polyphonique appelé « contrepoint ». Considérons la première note de la mesure 6 pour chaque voix. Nous avons la superposition des notes ''ré''-''fa''-''la'' (du grave vers l'aigu) ; la superposition de notes jouées ou chantées ensembles s'appelle un accord. Cet accord ''ré''-''fa''-''la'' porte le nom « d'accord parfait de ''ré'' mineur » : * « ''ré'' » car la note fondamentale est un ''ré'' ; * « parfait » car il est l'association d'une tierce, ''ré''-''fa'', et d'une quinte juste, ''ré''-''la'' ; * « mineur » car le premier intervalle, ''ré''-''fa'', est une tierce mineure. Considérons maintenant un chant accompagné au piano. Le piano peut jouer plusieurs notes en même temps, il peut jouer des accords. [[Fichier:Au clair de le lune chant et piano.svg|class=transparent|center|Deux premières mesure d’Au clair de la lune.]] [[Fichier:Au clair de le lune chant et piano.midi|vignette|Deux premières mesure d’Au clair de la lune.]] L'accord, les notes à jouer simultanément, sont écrites « en colonne ». Lorsqu'on les énonce, on les lit de bas en haut mais le pianiste les joue en pressant les touches du clavier en même temps, de manière « plaquée ». Le premier accord est composé des notes ''do''-''mi''-''sol'' ; il est appelé « accord parfait de ''do'' majeur » car la note fondamentale est ''do'', qu'il est l'association d'une tierce et d'une quinte juste et que le premier intervalle, ''do''-''mi'', est une tierce majeure. Si l'on lit toutes les notes, de la plus grave à la plus aiguë, nous avons : ''do'' (à la clef de ''fa'', main gauche du piano) - ''do'' - ''mi'' - ''sol'' (main droite du piano) - ''do'' (chant) ; nous voyons que la note ''do'' est jouée plusieurs fois, cela ne change pas la qualité de l'accord. == Consonance et dissonance == [[Fichier:Seconde majeure septieme mineure.svg|vignette|Seconde majeure et son renversement, le septième mineure.]] Les notions de consonance et de dissonance sont culturelles et changent selon l'époque. Nous pouvons néanmoins noter que : * l'accord de seconde, et son renversement la septième, créent des battements, les notes « frottent », c'est un intervalle harmonique dissonant ; mais dans le cas de la septième, comme les notes sont éloignées, le frottement est moins perceptible ; * les accords d'unisson, de tierce, quarte et quinte sonnent agréablement à l'oreille, ils sont consonants. Dans la musique savante européenne, au début du Moyen-Âge, seuls les accords d'unisson, de quarte et de quinte étaient considérés comme consonants, d'où leur qualification de « juste ». La tierce, et son renversement la sixte, étaient perçues comme dissonantes ; à partir de la Renaissance, ces deux intervalles sont considérés comme consonants. L'harmonie joue avec les consonances et les dissonances. Dans un premier temps, les harmonies dissonantes sont utilisées pour créer des tensions qui sont ensuite résolues, on utilise des successions « consonant-dissonant-consonant ». À force d'entendre des intervalles considérés comme dissonants, l'oreille s'habitue et certains finissent par être considérés comme consonants ; c'est ce qui est arrivé à la tierce et à la sixte à la fin du Moyen Âge avec le contrepoint, puis à la septième plus tardivement. Il faut ici aborder la notion d'harmonique des notes. [[File:Harmoniques de do.svg|thumb|Les six premières harmoniques de ''do''.]] Lorsque l'on joue une note, on entend d'autres notes plus aigües et plus faibles ; la note jouée est appelée la « fondamentale » et les notes plus aigües et plus faibles sont les « harmoniques ». C'est cette accumulation d'harmoniques qui donne la couleur au son, son timbre, qui fait qu'un piano ne sonne pas comme un violon. Par exemple, si l'on joue un ''do''<sup>1</sup><ref>Pour la notation des octaves, voir ''[[../Représentation_musicale#Désignation_des_octaves|Représentation musicale &gt; Désignation des octaves]]''.</ref> (fondamentale), on entend le ''do''<sup>2</sup> (une octave plus aigu), puis un ''sol''<sup>2</sup>, puis encore un ''do''<sup>3</sup> plus aigu, puis un ''mi''<sup>3</sup>, puis encore un ''sol''<sup>3</sup>, puis un ''si''♭<sup>3</sup>… Ainsi, puisque lorsque l'on joue un ''do'' on entend aussi un ''sol'' très léger, alors jouer un ''do'' et un ''sol'' simultanément n'est pas choquant. De même pour ''do'' et ''mi''. De là vient la notion de consonance. Le statut du ''si''♭ est plus ambigu. Il fait partie des harmoniques qui sonnent naturellement, mais il forme une seconde descendante avec le ''do'', intervalle dissonant. Par ailleurs, on remarque que le ''si''♭ ne fait pas partie de la gamme de ''do'' majeur, contrairement au ''sol'' et au ''mi''. Pour le jeu sur les dissonances, on peut écouter par exemple la ''Toccata'' en ''ré'' mineur, op. 11 de Sergueï Prokofiev (1912). : {{lien web |url=https://www.youtube.com/watch?v=AVpnr8dI_50 |titre=Yuja Wang Prokofiev Toccata |site=YouTube |date=2019-02-26 |consulté le=2021-12-19}} == Contrepoint == Dans le chant grégorien, la notion d'accord n'existe pas. L'harmonie provient de la superposition de plusieurs mélodies, notamment dans ce que l'on appelle le « contrepoint ». Le terme provient du latin ''« punctum contra punctum »'', littéralement « point par point », et désigne le fait que les notes de chaque voix se correspondent. L'exemple le plus connu de contrepoint est le canon, comme ''Frère Jacques'' ou bien ''Vent frais, vent du matin'' présenté ci-dessus : chaque note d'un couplet correspond à une note du couplet précédent. Certains morceaux sont bâtis sur une écriture « en miroir » : l'ordre des notes est inversé entre les deux voix, ou bien les intervalles sont inversés (« mouvement contraire » : une tierce montante sur une voix correspond à une tierce descendante sur l'autre). On peut également citer le « mouvement oblique » (une des voix, le bourdon, chante toujours la même note) et le mouvement parallèle (les deux voix chantent le même air mais transposé, l'une est plus aiguë que l'autre). Nous reproduisons ci-dessous le début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel. [[Fichier:Haendel Sonate en trio re mineur debut canon.svg | vignette | center | upright=2 | Début du second ''Allergo'' de la sonate en trio en ''ré'' mineur de Haendel.]] [[Fichier:Haendel Sonate en trio re mineur debut.midi | vignette | Début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.]] Nous avons mis en évidence la construction en canon avec des encadrés de couleur : sur les quatre premières mesures, nous voyons trois motifs repris alternativement par une voix et par l'autre. Ce type de procédé est très courant dans la musique baroque. Les procédés du contrepoint s'appliquent également à la danse : * unisson : les danseurs et danseuses font les mêmes gestes en même temps ; * répétition : le fait de répéter une série de gestes, une « phrase dansante » ; * canon : les gestes sont faits avec un décalage régulier d'un danseur ou d'une danseuse à l'autre ; * cascade : forme de canon dans laquelle le décalage est très petit ; * contraste : deux danseur·euses, ou deux groupes, ont des gestuelles très différentes ; * accumulation : la gestuelle se complexifie par l'ajout d'éléments au fur et à mesure ; ou bien le nombre de danseur·euses augmente ; * dialogue : les gestes de danseur·euses ou de groupes se répondent ; * contre-point : la gestuelle d'un ou une danseuse se superpose à la gestuelle d'un groupe ; * lâcher-rattraper : les danseurs et danseuses alternent danse à l'unisson et gestuelles indépendantes. : {{lien web | url=https://www.youtube.com/watch?v=wgblAOzedFc | titre=Les procédés de composition en danse | auteur= Doisneau Sport TV | site=YouTube | date=2020-03-16 | consulté le=2021-01-21 }} {{...}} == Ostinato == L'ostinato, mot italien signifiant « obstiné », c'est le fait de jouer en boucle un passage. L'ostinato peut ainsi former une structure autour de laquelle peut se développer le morceau. Lorsque l'ostinato est joué par l'instrument de basse (basson, violoncelle, viole de gambe basse, flûte à bec basse, main gauche du piano…), on parle de basse obstinée. Les grilles d'accord en jazz sont des ostinatos (ou ostinati) joués par la section rythmique. Exemples : * {{lien web |url=https://www.youtube.com/watch?v=QZpGe5rNJkI&t=41s |titre=Epic Patty Cake Song (I'll Think Of You) |auteur= Kurt Hugo Schneider |site=YouTube |date=2014-08-12 |consulté le=2026-01-24}} : un ostinato de percussions corporelles ; * {{lien web |url=https://www.youtube.com/watch?v=Kv-37-nXC48 |titre=Pow Wow ''Le lion est mort ce soir'' Les Victoires de la Musique 1993 |auteur=Solomon Linda |date=2015-02-02 |consulté le=2026-01-24}} : dans ce chant ''a capella'' (sans accompagnement d'instrument), les voix de basse forment une basse obstinée, sur les syllabes « oëap » durant le couplet et « awimbowé » sur le refrain ; * {{lien web |url=https://www.youtube.com/watch?v=JvNQLJ1_HQ0 |titre=Pachelbel Canon in D Major - the original and best version. |auteur=Voices of Music |site=YouTube |date=2008-09-02 |consulté le=2026-01-24}} : le ''Canon de Pachelbel de ''ré'' mineur'' (''Canon per tre Violini e Basso'', Johann Pachelbel, vers 1680) est construit sur une basse obstinée de huit mesures. == Les accords en général == Initialement, on a des chants polyphoniques, des voix qui chantent chacune une mélodie, les mélodies se mêlant. On remarque que certaines superpositions de notes sonnent de manière plus ou moins agréables, consonantes ou dissonantes. On en vient alors à associer ces notes, c'est-à-dire à considérer dès le départ la superposition de ces notes et non pas la rencontre de ces notes au gré des mélodies. Ces groupes de notes superposées forment les accords. En Europe, cette notion apparaît vers le {{pc|xiv}}<sup>e</sup> siècle avec notamment la ''[[wikipedia:fr:Messe de Notre Dame|Messe de Notre Dame]]'' de Guillaume de Machaut (vers 1360-1365). La notion « d'accord parfait » est consacrée par [[wikipedia:fr:Jean-Philippe Rameau|Jean-Philippe Rameau]] dans son ''Traité de l'harmonie réduite à ses principes naturels'', publié en 1722. === Qu'est-ce qu'un accord ? === Un accord est un ensemble d'au minimum trois notes jouées en même temps. « Jouées » signifie qu'il faut qu'à un moment donné, elles sonnent en même temps, mais le début ou la fin des notes peut être à des instants différents. Considérons que l'on joue les notes ''do'', ''mi'' et ''sol'' en même temps. Cet accord s'appelle « accord de ''do'' majeur ». En musique classique, on lui adjoint l'adjectif « parfait » : « accord parfait de ''do'' majeur ». Nous représentons ci-dessous trois manière de faire l'accord : avec trois instruments jouant chacun une note : [[Fichier:Do majeur trois portees.svg|class=transparent|center|Accord de ''do'' majeur avec trois instruments différents.]] Avec un seul instrument jouant simultanément les trois notes : [[Fichier:Chord C.svg|class=transparent|center|Accord de ''do'' majeur joué par un seul instrument.]] L'accord tel qu'il est joué habituellement par une guitare d'accompagnement : [[Fichier:Do majeur guitare.svg|class=transparent|center|Accord de ''do'' majeur à la guitare.]] Pour ce dernier, nous représentons le diagramme indiquant la position des doigts sur le manche au dessus de la portée et la tablature en dessous. Ici, c'est au total six notes qui sont jouées : ''mi'' grave, ''do'' médium, ''mi'' médium, ''sol'' médium, ''do'' aigu, ''mi'' aigu. Mais il s'agit bien des trois notes ''do'', ''mi'' et ''sol'' jouées à des octaves différentes. Nous remarquons également que la note de basse (la note la plus grave), ''mi'', est différente de la note fondamentale (celle qui donne le nom à l'accord), ''do'' ; l'accord est dit « renversé » (voir plus loin). === Comment joue-t-on un accord ? === Les notes ne sont pas forcément jouées en même temps ; elles peuvent être « égrainées », jouée successivement, ce que l'on appelle un arpège. La partition ci-dessous montre six manières différentes de jouer un accord de ''la'' mineur à la guitare, plaqué puis arpégé. [[Fichier:La mineur differentes executions.svg|class=transparent|center|Différentes exécution de l'accord de do majeur à la guitare.]] [[Fichier:La mineur differentes executions midi.midi|vignette|Différentes exécution de l'accord de la mineur à la guitare.]] Vous pouvez écouter l'exécution de cette partition avec le lecteur ci-contre. Seuls les instruments polyphoniques peuvent jouer les accords plaqués : instruments à clavier (clavecin, orgue, piano, accordéon), les instruments à plusieurs cordes pincées (harpe, guitare ; violon, alto, violoncelle et contrebasse joués en pizzicati). Les instruments à corde frottés de la famille du violon peuvent jouer des notes par deux à l'archet mais pas plus du fait de la forme bombée du chevalet ; cependant, un mouvement rapide permet de jouer les quatre cordes de manière très rapprochée. Les instruments à percussion de type xylophone ou le tympanon permettent de jouer jusqu'à quatre notes simultanément en tenant deux baguettes (mailloches, maillets) par main. Tous les instruments peuvent jouer des arpèges même si, dans le cas des instruments monodiques, les notes ne continuent pas à sonner lorsque l'on passe à la note suivante. L'arpège peut être joué par l'instrument de basse (basson, violoncelle, contrebasse, guitare basse, pédalier de l'orgue…), notamment dans le cas d'une basse continue ou d'une ''{{lang|en|walking bass}}'' (« basse marchante » : la basse joue des noires, donnant ainsi l'impression qu'elle marche). En jazz, et spécifiquement au piano, on a recours au ''{{lang|en|voicing}}'' : on choisit la manière dont on organise les notes pour donner une couleur spécifique, ou bien pour créer une mélodie en enchaînant les accords. Il est fréquent de ne pas jouer toutes les notes : si on n'en garde que deux, ce sont la tierce et la septième, car ce sont celles qui caractérisent l'accord (selon que la tierce est mineure ou majeure, que la septième est majeure ou mineure), et la fondamentale est en général jouée par la contrebasse ou guitare basse. {{clear}} === Classes d'accord === [[Fichier:Intervalles harmoniques accords classes.svg|vignette|upright=1.5|Intervalles harmoniques dans les accords classés de trois, quatre et cinq notes.]] Un accord composé d'empilement de tierces est appelé « accord classé ». En musique tonale, c'est-à-dire la musique fondée sur les gammes majeures ou mineures (cas majoritaire en musique classique), on distingue trois classes d'accords : * les accords de trois notes, ou triades, ou accords de quinte ; * les accords de quatre notes, ou accords de septième ; * les accords de cinq notes, ou accords de neuvième. En empilant des tierces, si l'on part de la note fondamentale, on a donc de intervalles de tierce, quinte, septième et neuvième. En musique tonale, les accords avec d'autres intervalles (hors renversement, voir ci-après), typiquement seconde, quarte ou sixte, sont considérés comme des transitions entre deux accords classés. Ils sont appelés, selon leur utilisation, « accords à retard » (en anglais : ''{{lang|en|suspended chord}}'', accord suspendu) ou « appoggiature » (note « appuyée », étrangère à l'harmonie). Voir aussi plus loin la notion de note étrangère. === Renversements d'accords === [[File:Accord do majeur renversements.svg|thumb|Accord parfait de do majeur et ses renversements.]] [[Fichier:Progression dominante renverse parfait do majeur.svg|vignette|upright=0.6|Progression accord de dominante renversé → accord parfait en ''do'' majeur.]] Un accord classé est donc un empilement de tierces. Si l'on change l'ordre des notes, on a toujours le même accord mais il est fait avec d'autres intervalles harmoniques. Par exemple, l'accord parfait de ''do'' majeur dans son état fondamental, c'est-à-dire non renversé, s'écrit ''do'' - ''mi'' - ''sol''. Sa note fondamentale, ''do'', est aussi se note de basse. Si maintenant on prend le ''do'' de l'octave supérieure, l'accord devient ''mi - sol - do'' ; c'est l'empilement d'une tierce ''(mi - sol)'' et d'une quarte ''(sol - do)'', soit la superposition d'une tierce ''(mi - sol)'' et d'une sixième ''(mi - do)''. C'est le premier renversement de l'accord parfait de ''do'' majeur ; la fondamentale est toujours ''do'' mais la basse est ''mi''. Le second renversement est ''sol - do - mi''. L'utilisation de renversement peut faciliter l'exécution de la progression d'accord. Par exemple, en tonalité ''do'' majeur, si l'on veut passer de l'accord de dominante ''sol - si - ré'' à l'accord parfait ''do - mi - sol'', alors on peut utiliser le second renversement de l'accord de dominante : ''ré - sol - si'' → ''do - mi - sol''. Ainsi, la basse descend juste d'un ton ''(ré → do)'' et sur un piano, la main reste globalement dans la même position. Le renversement d'un accord permet également de respecter certaines règles de l'harmonie classique, notamment éviter que des voix se suivent strictement (« mouvement parallèle »), ce qui aurait un effet de platitude. De manière générale, la notion de renversement permet deux choses : * d'enrichir l'œuvre : pour créer une harmonie donnée (c'est-à-dire des sons sonnant bien ensemble), nous avons plus de souplesse, nous pouvons organiser ces notes comme nous le voulons selon les voix ; * de simplifier l'analyse : quelle que soit la manière dont sont organisées les notes, cela nous ramène à un même accord. {{citation bloc|Or il, y a plusieurs manières de jouer un accord, selon que l'on aborde par la première note qui le constitue, ''do mi sol'', la deuxième, ''mi sol do'', ou la troisième note, ''sol do mi''. Ce sont les renversements, [que Rameau] va classer en différentes combinaisons d'une seule matrice. Faisant cela, Rameau divise le nombre d'accords [de septième] par quatre. Il simplifie, il structure […].|{{ouvrage|prénom1=André |nom1=Manoukian |titre=Sur les routes de la musique |éditeur=Harper Collins |année=2021 |passage=54 |isbn=979-1-03391201-9}} }} {{clear}} [[File:Plusieurs realisation 1er renversement doM.svg|thumb|Plusieurs réalisation du premier renversement de l'accord de ''do'' majeur.]] Notez que # Les notes peuvent être répétées plusieurs fois, à différentes octaves. # Dans la notion de renversement, seule importe en fait la note de basse. Ainsi, les accords ''mi-sol-do'', ''mi-do-sol'', ''mi-do-mi-sol'', ''mi-sol-mi-do''… sont tous une déclinaison du premier renversement de ''do-mi-sol'' et ils seront abrégés de la même manière (''mi''<sup>6</sup> en musique classique ou C/E en musique populaire et jazz, voir plus bas). {{clear}} == Notation des accords de trois notes == Les accords de trois notes sont appelés « accords de quinte » en classique, et « triades » en jazz. [[Fichier:Progression dominante renverse parfait do majeur chiffrage.svg|vignette|upright=0.7|Chiffrage du second renversement d'un accord de ''sol'' majeur et d'un accord de ''do'' majeur : notation en musique populaire et jazz (haut) et notation de basse chiffrée (bas).]] Les accords sont construits de manière systématique. Nous pouvons donc les représenter de manière simplifiée. Cette notation simplifiée des accords est appelée « chiffrage ». Reprenons la progression d'accords ci-dessus : « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » dans la tonalité de ''do'' majeur. On utilise en général trois notations différentes : * en musique populaire, jazz, rock… un accord est désigné par sa note fondamentale ; ici donc, les accords sont notés « ''sol'' - ''do'' » ou, en notation anglo-saxonne, « G - C » ;<br /> comme le premier accord est renversé, on indique la note de basse après une barre, la progression d'accords est donc chiffrée '''« ''sol''/''ré'' - ''do'' »''' ou '''« G/D - C »''' ;<br /> il s'agit ici d'accords composés d'une tierce majeure et d'une quinte juste ; si les accords sont constitués d'intervalles différents, nous ajoutons un symbole après la note : « m » ou « – » si la tierce est mineure, « dim » ou « ° » si la quinte est diminuée ; * en musique classique, on utilise la notation de « basse chiffrée » (utilisée notamment pour noter la basse continue en musique baroque) : on indique la note de basse sur la portée et on lui adjoint l'intervalle de la fondamentale à la note la plus haute (donc ici respectivement 6 et 5, puisque ''sol''-''si'' est une sixte et ''do''-''sol'' est une quinte), étant sous-entendu que l'on a des empilements de tierce en dessous ; mais dans le cas du premier accord, le premier intervalle n'est pas une tierce, mais une quarte ''(ré''-''sol)'', on note donc '''« ''ré'' <sup>6</sup><sub>4</sub> - ''do'' <sup>5</sup> »'''<ref>quand on ne dispose pas de la notation en supérieur (exposant) et inférieur (indice), on utilise parfois une notation sous forme de fraction : ''sol'' 6/4 et ''do'' 5/.</ref> ; * lorsque l'on fait l'analyse d'un morceau, on s'attache à identifier la note fondamentale de l'accord (qui est différente de la basse dans le cas d'un renversement) ; on indique alors le degré de la fondamentale : '''« {{Times New Roman|V<sup>6</sup><sub>4</sub> - I<sup>5</sup>}} »'''. La notation de basse chiffrée permet de construire l'accord à la volée : * on joue la note indiquée (basse) ; * s'il n'y a pas de 2 ni de 4, on lui ajoute la tierce ; * on ajoute les intervalles indiqués par le chiffrage. La notation de musique jazz oblige à connaître la composition des différents accords, mais une fois que ceux-ci sont acquis, il n'y a pas besoin de reconstruire l'accord. La notation de basse chiffrée avec les chiffres romains n'est pas utilisée pour jouer, mais uniquement pour analyser ; Sur les partitions avec basse chiffrée, il y a simplement les chiffrages indiqués au-dessus de la partie de basse. Le chiffrage avec le degré en chiffres romains présente l'avantage d'être indépendant de la tonalité et donc de se concentrer sur la fonction de l'accord au sein de la tonalité. Par exemple, ci-dessous, nous pouvons parler de la progression d'accords « {{Times New Roman|V - I}} » de manière générale, cette notation étant valable quelle que soit la tonalité. [[File:Progression dominante renverse parfait do majeur chiffrage basse continue.svg|thumb|Chiffrage en notation basse chiffrée de la progression d'accords « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » en do majeur.]] {{note|En notation de base continue avec fondamentale en chiffres romains, la fondamentale est toujours indiquée ''sous'' la portée de la partie de basse. Les intervalles sont indiqués au-dessus de la portée de la partie de basse ; lorsque l'on fait une analyse, on peut ayssi les indiquer à côté du degré en chiffres romains, donc sous la portée de la basse.}} {{note|En notation rock, le 5 en exposant indique un accord incomplet avec uniquement la fondamentale et la quinte, un accord sans tierce appelé « accord de puissance » ou ''{{lang|en|power chord}}''. Par exemple, C<sup>5</sup> est l'accord ''do-sol''.}} {{clear}} [[Fichier:Accords parfait do majeur basse chiffree fondamental et renverse.svg|vignette|upright=2.5|Chiffrage de l'accord parfait de ''do'' majeur en basse chiffrée, à l'état fondamental et ses renversements.]] Concernant les accords parfaits en notation de basse chiffrée : * un accord parfait à l'état fondamental est chiffré « <sup>5</sup> » ; on l'appelle « accord de quinte » ; * le premier renversement est chiffré « <sup>6</sup> » (la tierce est implicite) ; on l'appelle « accord de sixte » ; * le second renversement est noté « <sup>6</sup><sub>4</sub> » ; on l'appelle « accord de sixte et de quarte » (ou bien « de quarte et de sixte »). Par exemple, pour l'accord parfait de ''do'' majeur : * l'état fondamental ''do''-''mi''-''sol'' est noté ''do''<sup>5</sup> ; * le premier renversement ''mi''-''sol''-''do'' est noté ''mi''<sup>6</sup> ; * le second renversement ''sol''-''do''-''mi'' est noté ''sol''<sup>6</sup><sub>4</sub>. Il y a une exception : l'accord construit sur la sensible (7{{e}} degré) contient une quinte diminuée et non une quinte juste. Le chiffrage est donc différent : * l'état fondamental ''si''-''ré''-''fa'' est noté ''si''<sup><s>5</s></sup> (cinq barré), « accord de quinte diminuée » ; * le premier renversement ''ré''-''fa''-''si'' est noté ''ré''<sup>+6</sup><sub>3</sub>, « accord de sixte sensible et tierce » ; * le second renversement ''fa''-''si''-''ré'' est noté ''fa''<sup>6</sup><sub>+4</sub>, « accord de sixte et quarte sensible ». Par ailleurs, on ne considère pas qu'il est fondé sur la sensible, mais sur la dominante. Si certains indiquent le degré {{Times New Roman|VII}}, d'autres indiquent le degré {{Times New Roman|V}} en mettant des guillemets, « {{Times New Roman|“V”}} ». Donc selon l'état, le chiffrage est {{Times New Roman|“V”<sup><s>5</s></sup>}}, {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}} ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}. En notation jazz, on ajoute « dim », « <sup>o</sup> » ou bien « <sup>♭5</sup> » au chiffrage, ici : B dim, B<sup>o</sup> ou B<sup>♭5</sup> pour l'état fondamental. Pour les renversements : B dim/D et B dim/F ; ou bien B<sup>o</sup>/D et B<sup>o</sup>/F ; ou bien B<sup>♭5</sup>/D et B<sup>♭5</sup>/F. {{clear}} [[Fichier:Accords basse chiffree basse do fondamental et renverses.svg|vignette|upright=2|Basse chiffrée : accords de quinte, de sixte et de sixte et de quarte ayant pour basse ''do''.]] Et concernant les accords ayant pour basse ''do'' en tonalité de ''do'' majeur : * l'accord ''do''<sup>5</sup> est un accord à l'état fondamental, c'est donc l'accord ''do''-''mi''-''sol'' (sa fondamentale est ''do'') ; * l'accord ''do''<sup>6</sup> est le premier renversement d'un accord, c'est donc l'accord ''do''-''mi''-''la'' (sa fondamentale est ''la'') ; * l'accord ''do''<sup>6</sup><sub>4</sub> est le second renversement d'un accord, c'est donc l'accord ''do''-''fa''-''la'' (sa fondamentale est ''fa''). {{clear}} == Notes étrangères == La musique européenne s'appuie essentiellement sur des accords parfaits, c'est-à-dire fondés sur une tierce majeure ou mineure, et une quinte juste. Il arrive fréquemment qu'un accord ne soit pas un accord parfait. Les notes qui font partie de l'accord parfait sont appelées « notes naturelles » et la note qui n'en fait pas partie est appelée « note étrangère ». Il existe plusieurs types de notes étrangères : * anticipation : la note étrangère est une note naturelle de l'accord suivant ; * appogiature : note d'ornementation qui se résout par mouvement conjoint, c'est-à-dire qu'elle est suivie par une note située juste au-dessus ou en dessous (seconde ascendante ou descendante) qui est, elle, une note naturelle ; * broderie : on part d'une note naturelle, on monte ou on descend d'une seconde, puis on revient sur la note naturelle ; * double broderie : on part d'une note naturelle, on joue la note du dessus puis la note du dessous avant de revenir à la note naturelle ; ou bien on joue la note du dessous puis la note du dessus ; * échappée : note étrangère n'appartenant à aucune des autres catégories ; * note de passage : mouvement conjoint allant d'une note naturelle d'un accord à une note naturelle de l'accord suivant ; * pédale : la note de basse reste la même pendant plusieurs accords successifs ; * retard : la note étrangère est une note naturelle de l'accord précédent. Les notes étrangères ne sont pas chiffrées. [[File:Notes etrangeres accords.svg|center|Différents types de notes étrangères.]] {{note|Les anglophones distinguent deux types de retard : la ''{{lang|en|suspension}}'' est résolue vers le haut (le mouvement est ascendant), le ''{{lang|en|retardation}}'' est résolu vers le bas (le mouvement est descendant).}} == Principaux accords == Les trois principaux accords sont : * l'accord parfait majeur : il est construit sur les degrés {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (médiante) et {{Times New Roman|V}} (dominante) d'une gamme majeure ; il est noté {{Times New Roman|I}}<sup>5</sup>, {{Times New Roman|IV}}<sup>5</sup>, {{Times New Roman|V}}<sup>5</sup> ; * l'accord parfait mineur : il est construit sur les degrés {{Times New Roman|I}} (tonique) et {{Times New Roman|IV}} (sous-tonique) d'une gamme mineure harmonique ; il est également noté {{Times New Roman|I}}<sup>5</sup> et {{Times New Roman|IV}}<sup>5</sup>, les anglo-saxons le notent {{Times New Roman|i}}<sup>5</sup> et {{Times New Roman|iv}}<sup>5</sup> (la minuscule indiquant le caractère mineur) ; * l'accord de septième de dominante : il est construit sur le degré {{Times New Roman|V}} (dominante) d'une gamme majeure ou mineure harmonique ; il est noté {{Times New Roman|V}}<sup>7</sup><sub>+</sub>. On peut trouver ces trois accords sur d'autres degrés, et il existe d'autre types d'accords. Nous verrons cela plus loin. {| class="wikitable" |+ Constitution des principaux accords — Dénomination classique |- ! scope="col" | Accord ! scope="col" | 1<sup>er</sup> intervalle ! scope="col" | 2<sup>e</sup> intervalle ! scope="col" | 3<sup>e</sup> intervalle |- ! scope="row" | Accord parfait majeur | tierce majeure (3M) || quinte juste (5J) || — |- ! scope="row" | Accord parfait mineur | tierce mineure (3m) || quinte juste (5J) || — |- ! scope="row" | Accord de septième de dominante | tierce majeure (3M) || quinte juste (5J) || septième mineure (7m) |} {| class="wikitable" |+ Constitution des principaux accords — Dénomination jazz |- ! scope="col" | Accord ! scope="col" | 1<sup>er</sup> intervalle ! scope="col" | 2<sup>e</sup> intervalle ! scope="col" | 3<sup>e</sup> intervalle |- ! scope="row" | Triade majeure | tierce majeure (3M) || quinte juste (5J) || — |- ! scope="row" | Triade mineure | tierce mineure (3m) || quinte juste (5J) || — |- ! scope="row" | Accord de septième | tierce majeure (3M) || quinte juste (5J) || septième mineure (7m) |} {| border="0" |- | [[Fichier:Accord do majeur arpege puis plaque.midi | Accord parfait de ''do'' majeur (C).]] || [[Fichier:Accord do mineur arpege puis plaque.midi | Accord parfait de ''do'' mineur (Cm).]] || [[Fichier:Accord do septieme arpege puis plaque.midi | Accord de septième de dominante de ''fa'' majeur (C<sup>7</sup>).]] |- | Accord parfait<br /> de ''do'' majeur (C). || Accord parfait<br /> de ''do'' mineur (Cm). || Accord de septième de dominante<br /> de ''fa'' majeur (C<sup>7</sup>). |} '''Rappel :''' * la tierce mineure est composée d'un ton et demi (1 t ½) ; * la tierce majeur est composée de deux tons (2 t) ; * la quinte juste a la même altération que la fondamentale, sauf lorsque la fondamentale est ''si'' (la quinte juste est alors ''fa''♯) ; * la septième mineure est le renversement de la seconde majeure (1 t). [[File:Renversements accords pft fa maj basse chiffree.svg|thumb|Renversements de l'accord parfait de ''fa'' majeur, et la notation de basse chiffrée.]] [[File:Renversements accord sept de dom fa maj basse chiffree.svg|thumb|Renversements de l'accord de septième de dominante de ''fa'' majeur, et la notation de basse chiffrée.]] {| class="wikitable" |+ Notation des principaux accords en musique classique |- ! scope="col" | Accord ! scope="col" | État<br /> fondamental ! scope="col" | Premier<br /> renversement ! scope="col" | Deuxième<br /> renversement ! scope="col" | Troisième<br /> renversement |- ! scope="row" | Accord parfait | {{Times New Roman|I<sup>5</sup>}}<br/> acc. de quinte || {{Times New Roman|I<sup>6</sup>}}<br :> acc. de sixte || {{Times New Roman|I<sup>6</sup><sub>4</sub>}}<br /> acc. de quarte et de sixte || — |- ! scope="row" | Accord de septième<br /> de dominante | {{Times New Roman|V<sup>7</sup><sub>+</sub>}}<br /> acc.de septième de dominante || {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}<br />acc. de sixte et quinte diminuée || {{Times New Roman|V<sup>+6</sup>}}<br />acc. de sixte sensible || {{Times New Roman|V<sup>+4</sup>}}<br />acc. de quarte sensible<br />acc. de triton |} {| class="wikitable" |+ Notation des principaux accords en jazz |- ! scope="col" | Accord ! scope="col" | Chiffrage ! scope="col" | Renversements |- ! scope="row" | Triade majeure | X | rowspan="3" | Les renversements se notent en mettant la basse après une barre de fraction, par exemple pour la triade de ''do'' majeur : * état fondamental : C ; * premier renversement : C/E ; * second renversement : C/G. |- ! scope="row" | Triade mineure | Xm, X– |- ! scope="row" | Septième | X<sup>7</sup> |} {{clear}} Dans le cas d'un accord de septième de dominante, le nom de l'accord change selon que l'on est en musique classique ou en jazz : en musique classique, on donne le nom de la tonalité alors qu'en jazz, on donne le nom de la fondamentale. Ainsi, l'accord appelé « septième de dominante de ''do'' majeur » en musique classique, est appelé « ''sol'' sept » (G<sup>7</sup>) en jazz : la dominante (degré {{Times New Roman|V}}, dominante) de la tonalité de ''do'' majeur est la note ''sol''. Comment appelle-t-on en musique classique l'accord appelé « ''do'' sept » (C<sup>7</sup>) en jazz ? Les tonalités dont le ''do'' est la dominante sont les tonalités de ''fa'' majeur (''si''♭ à la clef) et de ''fa'' mineur harmonique (''si''♭, ''mi''♭, ''la''♭ et ''ré''♭ à la clef et ''mi''♮ accidentel). Il s'agit donc de l'accord de septième de dominante des tonalités de ''fa'' majeur et ''fa'' mineur harmonique. {| class="wikitable" |+ Accords fréquents pour quelques la tonalités majeures ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Accord parfait<br />{{Times New Roman|I<sup>5</sup>}} ! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}} |- |''Do'' majeur || || C<br />''do-mi-sol'' || G7<br />''sol-si-ré-fa'' |- |''Sol'' majeur || ''fa''♯ || G<br />''sol-si-ré'' || D7<br />''ré-fa''♯''-la-do'' |- |''Ré'' majeur || ''fa''♯, ''do''♯ || D<br />''ré-fa''♯''-la'' || A7<br />''la-do''♯''-mi-sol'' |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A<br />''la-do''♯''-mi'' || E7<br />''mi-sol''♯''-si-ré'' |- | ''Fa'' majeur || ''si''♭ || F<br />''fa-la-do'' || C7<br />''do-mi-sol-si''♭ |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭<br />''si''♭''-ré-fa'' || F7<br />''fa-la-do-mi''♭ |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭<br />''mi''♭''-sol-si''♭ || B♭7<br />''si''♭''-ré-fa-la''♭ |} {| class="wikitable" |+ Accords fréquents pour quelques la tonalités mineures harmoniques ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Accord parfait<br />{{Times New Roman|i<sup>5</sup>}} ! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}} |- |''La'' mineur<br />harmonique || || Am, A–<br />''la-do-mi'' || E7<br />''mi-sol''♯''-si-ré'' |- |''Mi'' mineur<br />harmonique || ''fa''♯ || Em, E–<br />''mi-sol-si'' || B7<br />''si-ré''♯''-fa''♯''-la'' |- |''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || Bm, B–<br />''si-ré-fa''♯ || F♯7<br />''fa''♯''la''♯''-do''♯''-mi'' |- |''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || F♯m, F♯–<br />''fa''♯''-la-do''♯ || C♯7<br />''do''♯''-mi''♯''-sol''♯''-si'' |- | ''Ré'' mineur<br />harmonique || ''si''♭ || Dm, D–<br />''ré-fa-la'' || A7<br />''la-do''♯''-mi-sol'' |- | ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || Gm, G–<br />''sol-si''♭''-ré'' || D7<br />''ré-fa''♯''-la-do'' |- | ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || Cm, C–<br />''do-mi''♭''-sol'' || G7<br />''sol-si''♮''-ré-fa'' |} {{clear}} == Accords sur les degrés d'une gamme == === Harmonisation d'une gamme === [[Fichier:Accord trois notes gamme do majeur chiffre.svg|vignette|upright=1.2|Accords de trois note sur la gamme de ''do'' majeur, chiffrés.]] On peut ainsi construire une triade par degré d'une gamme. Pour une gamme majeure, les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|V<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|II<sup>5</sup>}}, {{Times New Roman|III<sup>5</sup>}}, {{Times New Roman|VI<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|ii<sup>5</sup>}}, {{Times New Roman|iii<sup>5</sup>}}, {{Times New Roman|vi<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}. Les accords ont tous une quinte juste à l'exception de l'accord {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} qui a une quinte diminuée, raison pour laquelle le « 5 » est barré. C'est un accord dit « de quinte diminuée ». En jazz, l'accord diminué est noté « dim », « ° », « m<sup>♭5</sup> » ou « <sup>–♭5</sup> ». Nous avons donc trois types d'accords (dans la notation jazz) : X (triade majeure), Xm (triade mineure) et X° (triade diminuée), la lettre X remplaçant le nom de la note fondamentale. {{clear}} [[Fichier:Accord trois notes gamme la mineur chiffre.svg|vignette|upright=1.2|Accords de trois notes sur une gamme de ''la'' mineur harmonique, chiffrés.]] Pour une gamme mineure harmonique, les accords {{Times New Roman|III<sup>+5</sup>}}, {{Times New Roman|V<sup>♯</sup>}} et {{Times New Roman|VI<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|II<sup><s>5</s></sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|i<sup>5</sup>}}, {{Times New Roman|ii<sup><s>5</s></sup>}}, {{Times New Roman|iv<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}. Les accords {{Times New Roman|ii<sup><s>5</s></sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} ont une quinte diminuée ; ce sont des accords dits « de quinte diminuée ». L'accord {{Times New Roman|III<sup>+5</sup>}} a une quinte augmentée ; le signe « plus » indique que la note de cinquième, le ''sol'' dièse, est la sensible. En jazz, l'accord est noté « aug » ou « <sup>+</sup> ». Les autres accords ont une quinte juste. Aux trois accords générés par une gamme majeure (X, Xm et X°), nous voyons ici apparaître un quatrième type d'accord : la triade augmentée X<sup>+</sup>. Nous remarquons que des gammes ont des accords communs. Par exemple, l'accord {{Times New Roman|ii<sup>5</sup>}} de ''do'' majeur est identique à l'accord {{Times New Roman|iv<sup>5</sup>}} de ''la'' mineur (il s'agit de l'accord Dm). Quel que soit le mode, les accords construits sur la sensible (accord de quinte diminuée) sont rarement utilisés. S'ils le sont, c'est en tant qu'accord de septième de dominante sans fondamentale (voir ci-après). C'est la raison pour laquelle le chiffrage indique le degré {{Times New Roman|V}} entre guillemets, et non pas le degré {{Times New Roman|VII}} (mais pour des raisons de clarté, nous l'indiquons entre parenthèses au début). En mode mineur, l'accord de quinte augmentée {{Times New Roman|iii<sup>+5</sup>}} est très peu utilisé (voir plus loin ''[[#Progression_d'accords|Progression d'accords]]''). C'est un accord considéré comme dissonant. On voit que : * un accord parfait majeur peut appartenir à cinq gammes différentes ;<br /> par exemple l'accord parfait de ''do'' majeur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> degré de la gamme de ''do'' majeur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' mineur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''mi'' mineur ; * un accord parfait mineur peut appartenir à cinq gammes différentes ;<br />par exemple l'accord parfait de ''la'' mineur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> de la gamme de ''la'' mineur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''mi'' mineur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|III}}<sup>e</sup> degré de ''fa'' majeur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''do'' majeur ; * un accord de quinte diminuée peut appartenir à trois gammes différentes ;<br />par exemple, l'accord de quinte diminuée de ''si'' est l'accord construit sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' majeur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''la'' mineur et sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' mineur ; * un accord de quinte augmentée (à l'état fondamental) ne peut appartenir qu'à une seule gamme ;<br /> par exemple, l'accord de quinte augmentée de ''do'' est l'accord construit sur le {{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur. {| class="wikitable" |+ Notation jazz des triades |- | rowspan="2" colspan="2" | ! scope="col" colspan="2" | Tierce |- ! scope="col" | 3m ! scope="col" | 3M |- ! rowspan="3" | Quinte ! scope="row" | 5d | Xᵒ, X<sub>m</sub><sup>(♭5)</sup> || |- ! scope="row" | 5J | Xm, X– || X |- ! scope="row" | 5A | || X+, X<sup>(♯5)</sup> |} === Harmonisation par des accords de septième === [[Fichier:Harmonisation gamme do majeur par septiemes chiffre.svg|vignette|upright=2|Harmonisation de la gamme de do majeur par des accords de septième.]] Les accords de septième contiennent une dissonance et créent ainsi une tension. Ils sont très utilisés en jazz. Nous avons représenté ci-contre l'harmonisation de la gamme de ''do'' majeur. La constitution des accords est la suivantes : * tierce majeure (3M) ** quinte juste (5J) *** septième mineure (7m) : sur le degré V, c'est l'accord de septième de dominante V<sup>7</sup><sub>+</sub>, noté X<sup>7</sup> (X pour G), *** septième majeure (7M) : sur les degrés I et IV, appelés « accords de septième majeure » et notés aussi X<sup>maj7</sup> ou X<sup>Δ</sup> (X pour C ou F) ; * tierce mineure (3m) ** quinte juste (5J) *** septième mineure : sur les degrés ii, iii et vi, appelés « accords mineur septième » et notés Xm<sup>7</sup> ou X–<sup>7</sup> (X pour D, E ou A), ** quinte diminuée (5d) *** septième mineure (7m) : sur le degré vii, appelé « accord demi-diminué » (puisque seule la quinte est diminuée) et noté X<sup>∅</sup> ou Xm<sup>7(♭5)</sup> ou X–<sup>7(♭5)</sup> (X pour B) ;<br /> en musique classique, on considère que c'est un accord de neuvième de dominante sans fondamentale. Nous avons donc quatre types d'accords : X<sup>7</sup>, X<sup>maj7</sup>, Xm<sup>7</sup> et X<sup>∅</sup> En jazz, on ajoute souvent la quarte à l'accord de sous-dominante IV (sur le ''fa'' dans une gamme de ''do'' majeur) ; il s'agit ici d'une quarte augmentée (''fa''-''si'') et l'accord est surnommé « accord lydien » mais cette dénomination est erronée (il s'agit d'une mauvaise interprétation de textes antiques). C'est un accord de onzième sans neuvième (la onzième étant l'octave de la quarte), il est noté X<sup>maj7(♯11)</sup> ou X<sup>Δ(♯11)</sup> (ici, F<sup>maj7(♯11)</sup>, ''fa''-''la''-''do''-''mi''-''si'' ou ''fa''-''la''-''si''-''do''-''mi''). {| class="wikitable" |+ Chiffrage jazz des accords de septième |- ! scope="col" rowspan="2" | Tierce ! scope="col" rowspan="2" | Quinte ! scope="col" colspan="2" | Septième |- ! scope="col" | 7m ! scope="col" | 7M |- | rowspan="2" | 3m | 5d || X<sup>∅</sup>, X<sub>m</sub><sup>7(♭5)</sup>, X–<sup>7(♭5)</sup> || |- | rowspan="2" | 5J | X<sub>m</sub><sup>7</sup>, X–<sup>7</sup> || |- | rowspan="2" | 3M | X<sup>7</sup> || X<sup>maj7</sup>, X<sup>Δ</sup> |- | 5A || || X<sub>+</sub><sup>maj7</sup>, X<sub>+</sub><sup>Δ</sup> |} === Modulation et emprunt === Un morceau peut comporter des changements de tonalité; appelés « modulation ». Il y a parfois un court passage dans une tonalité différente, typiquement sur une ou deux mesures, avant de retourner dans la tonalité d'origine : on parle d'emprunt. Lorsqu'il y a une modulation ou un emprunt, les degrés changent. Un même accord peut donc avoir une fonction dans une partie du morceau et une autre fonction ailleurs. L'utilisation d'accord différents, et en particulier d'accord utilisant des altérations accidentelles, indique clairement une modulation. Nous avons vu précédemment que les modulations courantes sont : * les modulations dans les tons voisins ; * les modulations homonymes ; * les marches harmoniques. Une modulation entre une tonalité majeure et mineure change la couleur du passage, * la modulation la plus « douce » est entre les tonalités relatives (par exemple''do'' majeur et ''la'' mineur) car ces tonalités utilisent quasiment les mêmes notes ; * la modulation la plus « voyante » est la modulation homonyme (par exemple entre ''do'' majeur et ''do'' mineur). Une modulation commence souvent sur l'accord de dominante de la nouvelle tonalité. Pour analyser un œuvre, ou pour improviser sur une partie, il est important de reconnaître les modulations. La description de la successind es tonalités s'appelle le « parcours tonal ». === Exercices élémentaires === L'apprentissage des accords passe par quelques exercices élémentaires. '''1. Lire un accord''' Il s'agit de lecture de notes : des notes composant les accords sont écrites « empilées » sur une portée, il faut les lire en énonçant les notes de bas en haut. '''2. Reconnaître la « couleur » d'un accord''' On écoute une triade et il faut dire si c'est une triade majeure ou mineure. Puis, on complexifie l'exercice en ajoutant la septième. '''3. Chiffrage un accord''' Trouver le nom d'un accord à partir des notes qui le composent. '''4. Réalisation d'un accord''' Trouver les notes qui composent un accord à partir de son nom. '''5. Dictée d'accords''' On écoute une succession d'accords et il faut soit écrire les notes sur une portée, soit écrire les noms de accords. [[File:Exercice constitution accord basse chiffree.svg|thumb|Exercice : constitution d'accord à partir de la basse chiffrée.]] '''Exercices de basse chiffrée''' ''Réalisation d'un accord'' Sur la figure suivante, écrire les notes des accords correspondant à la basse chiffrée. Déterminer le degré de la fondamentale pour chaque accord en considérant que nous sommes dans la tonalité de ''sol'' majeur. {{boîte déroulante/début|titre=Solution}} [[File:Exercice constitution accord basse chiffree solution.svg|vignette|Solution.]] # La note de basse est un ''do''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''mi'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''sol''.<br />Le chiffrage « <sup>5</sup> » indique que c'est un accord dans son état fondamental (l'écart entre deux notes consécutives ne dépasse pas la tierce), la fondamentale est donc la basse, ''do'', qui est le degré IV de la tonalité. # La note de basse est un ''si''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''ré'', puis nous appliquons le chiffrage 6 et ajoutons la sixte, ''sol''.<br />Le chiffrage « <sup>6</sup> » indique que c'est un accord dans son premier renversement. En le remettant dans son état fondamental, nous obtenons ''sol-si-ré'', la fondamentale est donc la tonique, le degré I. # La note de basse est un ''la''. Nous ajoutons la tierce (chiffre 3), ''do'', et la sixte (6), ''fa''♯. Nous vérifions que le ''fa''♯ est la sensible (signe +)<br />Nous voyons un « blanc » entre les notes ''do'' et ''fa''♯. En descendant le ''fa''♯ à l'octave inférieure, nous obtenons un empilement de tierces ''fa''♯-''la-do'', le fondamentale est donc ''fa''♯, le degré VII. Nous pouvons le voir comme le deuxième renversement de l'accord de septième de dominante, sans fondamentale. # La note de basse est un ''fa''♯. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''la'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''do'' ; nous vérifions qu'il s'agit bien d'une quinte diminuée (le 5 est barré). Nous appliquons le chiffre 6 et ajoutons la sixte, ''ré''.<br />Nous voyons que les notes ''do'' et ''ré'' sont conjointes (intervalle de seconde). En descendant le ''ré'' à l'octave inférieure, nous obtenons un empilement de tierces ''ré-fa''♯-''la-do'', le fondamentale est donc ''ré'', le degré V. Nous constatons que l'accord chiffré est le premier renversement de l'accord de septième de dominante. {{boîte déroulante/fin}} {{clear}} ''Chiffrage d'accords'' [[Fichier:Exercice chiffrage accord basse chiffree.svg|vignette|Accords à chiffrer.]] Chiffrer les accords ci-contre. {{boîte déroulante/début|titre=Solution}} [[File:Exercice chiffrage accord basse chiffree solution.svg|vignette|Solution.]] # On relève les intervalles en partant de la basse : tierce majeure (3M) et quinte juste (5J). Le chiffrage complet est donc ''fa''<sup>5</sup><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''fa''<sup>5</sup>.<br /> On peut aussi reconnaître que c'est l'accord parfait sur la tonique de la tonalité de ''fa'' majeur dans son état fondamental, le chiffrage d'un accord parfait étant <sup>5</sup>. # On relève les intervalles en partant de la basse : quarte juste (4J), sixte majeure (6M). Le chiffrage complet est donc ''fa''<sup>6</sup><sub>4</sub>.<br /> On peut aussi reconnaître que c'est le second renversement de l'accord ''mi-sol-si'', sur la tonique de la tonalité de ''mi'' mineur, le chiffrage du second renversement d'un accord parfait étant <sup>6</sup><sub>4</sub>. # Les intervalles en partant de la basse sont : tierce mineure (3m), quinte diminuée (5d), sixte mineure (6m). Le chiffrage complet est donc ''mi''<sup>6</sup><small><s>5</s></small><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''mi''<sup>6</sup><sub><s>5</s></sub>.<br /> On reconnaît le premier renversement de l'accord ''do-mi-sol-si''♭, accord de septième de dominante de la tonalité de ''fa'' majeur. # Les intervalles en partant de la basse sont : tierce mineure (3m), quinte juste (5J), septième mineure (7m). Le chiffrage complet est donc ''ré''<sup>7</sup><small>5</small><sub>3</sub> ; c'est typique d'un accord de septième de dominante, son chiffrage est donc ''ré''<sup>7</sup><sub>+</sub>.<br /> On reconnaît l'accord de septième de dominante de la tonalité de ''sol'' mineur dans son état fondamental. {{boîte déroulante/fin}} {{clear}} [[File:Exercice constitution accord notation jazz.svg|thumb|Exercice : constitution d'un accord d'après son chiffrage en notation jazz.]] '''Exercices de notation jazz''' ''Réalisation d'un accord'' Sur la figure suivante, écrire les notes des accords correspondant aux chiffrages. {{boîte déroulante/début|titre=Solution}} [[File:Exercice constitution accord notation jazz solution.svg|thumb|Solution.]] # Il s'agit de la triade majeure de ''do'' dans son état fondamental. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''do-mi-sol''. # Il s'agit de la triade majeure de ''sol''. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''sol-si-ré''. On renverse l'accord afin que la basse soit le ''si'', l'accord est donc ''si-ré-sol''. # Il s'agit de l'accord demi-diminué de ''fa''♯. Les intervalles sont la tierce mineure (3m), la quinte diminuée (5d) et la septième mineure (7m). Les notes sont donc ''fa''♯-''la-do-mi''. Nous renversons l'accord afin que la basse soit le ''la'', l'accord est donc ''a-do-mi-fa''♯. # Il s'agit de l'accord de septième de ''ré''. Les intervalles sont donc la tierce majeure (3M), la quinte juste (5J) et la septième mineure (7m). Les notes sont ''ré-fa''♯''-la-do''. Nous renversons l'accord afin que la basse soit le ''fa''♯, l'accord est donc ''fa''♯''-la-do-ré''. {{boîte déroulante/fin}} {{clear}} ''Chiffrage d'accords'' [[File:Exercice chiffrage accord notation jazz.svg|thumb|Accords à chiffrer.]] Chiffrer les accords ci-contre. {{boîte déroulante/début|titre=Solution}} [[File:Exercice chiffrage accord notation jazz solution.svg|thumb|Solution.]] # Les notes sont toutes sur des interlignes consécutifs, c'est donc un empilement de tierces ; l'accord est dans son état fondamental. Les intervalles sont une tierce majeure (''fa-la'' : 3M) et une quinte juste (''fa-do'' : 5J), c'est donc la triade majeure de ''fa''. Le chiffrage est F. # Il y a un blanc dans l'empilement des notes, c'est donc un accord renversé. En permutant les notes pour n'avoir que des tierces, on trouve l'accord ''mi-sol-si''. Les intervalles sont une tierce mineure (''mi-sol'' : 3m) et une quinte juste (''mi-si'' : 5J), c'est donc la triade mineure de ''mi'' avec un ''si'' à la basse. Le chiffrage est Em/B ou E–/B. # Il y deux notes conjointes, c'est donc un renversement. L'état fondamental de cet accord est ''do-mi-sol-si''♭. Les intervalles sont une tierce majeure (''do-mi'' : 3M), une quinte juste (''do-sol'' : 5J) et une septième mineure (''do-si''♭ : 7m). C'est donc l'accord de ''do'' septième avec un ''mi'' à la basse, chiffré C<sup>7</sup>/E. # Les notes sont toutes sur des interlignes consécutifs, l'accord est dans son état fondamental. Les intervalles sont la tierce mineure (''ré-fa'' : 3m), une quinte juste (''ré-la'' : 5J) et une septième mineure (''ré-do'' : 7m). C'est donc l'accord de ''ré'' mineur septième, chiffré Dm<sup>7</sup> ou D–<sup>7</sup>. {{boîte déroulante/fin}} {{clear}} == Harmonie fonctionnelle == Le choix des accords et de leur succession — la progression des accords — est un élément important d'un morceau, de sa composition. Le compositeur ou la compositrice a bien sûr une liberté totale, mais pour faire des choix, il faut comprendre les conséquences de ces choix, et donc ici, les effets produits par les accords et leur progression. Une des manières d'aborder le sujet est l'harmonie fonctionnelle. === Les trois fonctions des accords === En harmonie tonale, on considère que les accords ont une fonction. Il existe trois fonctions : * la fonction de tonique, {{Times New Roman|I}} ; * la fonction de sous-dominante, {{Times New Roman|IV}} ; * la fonction de dominante, {{Times New Roman|V}}. L'accord de tonique, {{Times New Roman|I}}, est l'accord « stable » de la tonalité par excellence. Il conclut en général les morceaux, et ouvre souvent les morceaux ; il revient fréquemment au cours du morceau. L'accord de dominante, {{Times New Roman|V}}, est un accord qui introduit une instabilité, une tension. En particulier, il contient la sensible (degré {{Times New Roman|VI}}), qui est une note « aspirée » vers la tonique. Cette tension, qui peut être renforcée par l'utilisation d'un accord de septième, est fréquemment résolue par un passage vers l'accord de tonique. Nous avons donc deux mouvements typiques : {{Times New Roman|I}} → {{Times New Roman|V}} (création d'une tension, d'une attente) et {{Times New Roman|V}} → {{Times New Roman|I}} (résolution d'une tension). Les accords de tonique et de dominante ont le cinquième degré en commun, cette note sert donc de pivot entre les deux accords. L'accord de sous-dominante, {{Times New Roman|IV}}, est un accord qui introduit lui aussi une tension, mais moins grande : il ne contient pas la sensible. Notons que s'il est une quarte au-dessus de la tonique, il est aussi une quinte en dessous d'elle ; il est symétrique de l'accord de dominante. Il a donc un rôle similaire à l'accord de dominante, mais atténué. L'accord de sous-dominante aspire soit vers l'accord de dominante, très proche, et l'on a alors une augmentation de la tension ; soit vers l'accord de tonique, un retour vers la stabilité (il a alors un rôle semblable à la dominante). Du fait de ces deux bifurcations possibles — augmentation de la tension ({{Times New Roman|IV}} → {{Times New Roman|V}}) ou retour à la stabilité ({{Times New Roman|IV}} → {{Times New Roman|I}}) —, l'utilisation de l'accord de sous-dominante introduit un certain flottement : si l'on peut facilement prédire l'accord qui suit un accord de dominante, on ne peut pas prédire ce qui suit un accord de sous-dominante. Notons que la composition ne consiste pas à suivre ces règles de manière stricte, ce qui conduirait à des morceaux stéréotypés et plats. Le plaisir d'écoute joue sur une alternance entre satisfaction d'une attente (respect des règles) et surprise (rompre les règles). === Accords remplissant ces fonctions === Les accords sur les autres degrés peuvent se ramener à une de ces trois fonctions : * {{Times New Roman|II}} : fonction de sous-dominante {{Times New Roman|IV}} ; * {{Times New Roman|III}} (très peu utilisé en mode mineur en raison de sa dissonance) et {{Times New Roman|VI}} : fonction de tonique {{Times New Roman|I}} ; * {{Times New Roman|VII}} : fonction de dominante {{Times New Roman|V}}. En effet, les accords étant des empilements de tierces, des accords situés à une tierce l'un de l'autre — {{Times New Roman|I}} ↔ {{Times New Roman|III}}, {{Times New Roman|II}} ↔ {{Times New Roman|IV}}, {{Times New Roman|V}} ↔ {{Times New Roman|VII}}, {{Times New Roman|VI}} ↔ {{Times New Roman|VIII}} ( = {{Times New Roman|I}}) — ont deux notes en commun. On retrouve le fait que l'accord sur le degré {{Times New Roman|VII}} est considéré comme un accord de dominante sans tonique. En mode mineur, l'accord sur le degré {{Times New Roman|III}} est évité, il n'a donc pas de fonction. {|class="wikitable" |+ Fonction des accords |- ! scope="col" | Fondamentale ! scope="col" | Fonction |- | {{Times New Roman|I}} || tonique |- | {{Times New Roman|II}} || sous-dominante faible |- | {{Times New Roman|III}} || tonique faible |- | {{Times New Roman|IV}} || sous-dominante |- | {{Times New Roman|V}} || dominante |- | {{Times New Roman|VI}} || tonique faible |- | {{Times New Roman|VII}} || dominante faible |} Par exemple en ''do'' majeur : * fonction de tonique : '''''do''<sup>5</sup> (C)''', ''mi''<sup>5</sup> (E–), ''la''<sup>5</sup> (A–) ; * fonction de sous-dominante : '''''fa''<sup>5</sup> (F)''', ''ré''<sup>5</sup> (D–) ; * fonction de dominante : '''''sol''<sup>5</sup> (G)''' ou ''sol''<sup>7</sup><sub>+</sub> (G<sup>7</sup>), ''si''<sup> <s>5</s></sup> (B<sup>o</sup>). En ''la'' mineur harmonique : * fonction de tonique : '''''la''<sup>5</sup> (A–)''', ''fa''<sup>5</sup> (F) [, rarement : ''do''<sup>+5</sup> (C<sup>+</sup>)] ; * fonction de sous-dominante : '''''ré''<sup>5</sup> (D–)''', ''si''<sup> <s>5</s></sup> (B<sup>o</sup>) ; * fonction de dominante : '''''mi''<sup>5</sup> (E)''' ou ''mi''<sup>7</sup><sub>+</sub> (E<sup>7</sup>), ''sol''♯<sup> <s>5</s></sup> (G♯<sup>o</sup>). Le fait d'utiliser des accords différents pour remplir une fonction permet d'enrichir l'harmonie, et de jouer sur l'équilibre entre satisfaction d'une attente (on respecte les règles sur les fonctions) et surprise (mais on n'utilise pas l'accord attendu). === Les dominantes secondaires === On utilise aussi des accords de septième dominante se fondant sur un autre degré que la dominante de la gamme ; on parle de « dominante secondaire ». Typiquement, avant un accord de septième de dominante, on utilise parfois un accord de dominante de dominante, dont le degré est alors noté « {{Times New Roman|V}} de {{Times New Roman|V}} » ou « {{Times New Roman|V}}/{{Times New Roman|V}} » ; la fondamentale est de l'accord est alors situé cinq degrés au-dessus de la dominante ({{Times New Roman|V}}), c'est donc le degré {{Times New Roman|IX}}, c'est-à-dire le degré {{Times New Roman|II}} de la tonalité en cours). Ou encore, on utilise un accord de dominante du degré {{Times New Roman|IV}} (« {{Times New Roman|V}} de {{Times New Roman|IV}} », la fondamentale est alors le degré {{Times New Roman|I}}) avant un accord sur le degré {{Times New Roman|IV}} lui-même. Par exemple, en tonalité de ''do'' majeur, on peut trouver un accord ''ré - fa''♯'' - la - do'' (chiffré {{Times New Roman|V}} de {{Times New Roman|V}}<sup>7</sup><sub>+</sub>), avant un accord ''sol - si - ré - fa'' ({{Times New Roman|V}}<sup>7</sup><sub>+</sub>). L'accord ''ré - fa''♯'' - la - do'' est l'accord de septième de dominante des tonalités de ''sol''. Dans la même tonalité, on pourra utiliser un accord ''do - mi - sol - si''♭ ({{Times New Roman|V}} de {{Times New Roman|IV}}<sup>7</sup><sub>+</sub>) avant un accord ''fa - la - do'' ({{Times New Roman|IV}}<sup>5</sup>). Le recours à une dominante secondaire peut atténuer une transition, par exemple avec un enchaînement ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> → ''fa''<sup>5</sup> (C → C<sup>7</sup> → F) qui correspond à un enchaînement {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}} : le passage ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> (C → C<sup>7</sup>) se fait en ajoutant une note (le ''si''♭) et rend naturel le passage ''do'' → ''fa''. Sur les sept degré de la gamme, on ne considère en général que cinq dominantes secondaires : en effet, la dominante du degré {{Times New Roman|I}} est la dominante « naturelle, primaire » de la tonalité (et n'est donc pas secondaire) ; et utiliser la dominante de {{Times New Roman|VII}} consisterait à considérer l'accord de {{Times New Roman|VII}} comme un accord propre, on évite donc les « {{Times New Roman|V}} de “{{Times New Roman|V}}” » (mais les « “{{Times New Roman|V}}” de {{Times New Roman|V}} » sont tout à fait « acceptables »). === Enchaînements classiques === Nous avons donc vu que l'on trouve fréquemment les enchaînements suivants : * pour créer une instabilité : ** {{Times New Roman|I}} → {{Times New Roman|V}}, ** {{Times New Roman|I}} → {{Times New Roman|IV}} (instabilité moins forte mais incertitude sur le sens d'évolution) ; * pour maintenir l'instabilité : ** {{Times New Roman|IV}} → {{Times New Roman|V}} ; * pour résoudre l'instabilité : ** {{Times New Roman|IV}} → {{Times New Roman|I}}, ** {{Times New Roman|V}} → {{Times New Roman|I}}, cas particuliers (voir plus bas) : *** {{Times New Roman|V}}<sup>+4</sup> → {{Times New Roman|I}}<sup>6</sup>, *** {{Times New Roman|I}}<sup>6</sup><sub>4</sub> → {{Times New Roman|V}}<sup>7</sup><sub>+</sub> → {{Times New Roman|I}}<sup>5</sup>. Les degrés indiqués ci-dessus sont les fonctions ; on peut donc utiliser les substitutions suivantes : * {{Times New Roman|I}} par {{Times New Roman|VI}} et, en tonalité majeure, {{Times New Roman|III}} ; * {{Times New Roman|IV}} par {{Times New Roman|II}} ; * {{Times New Roman|V}} par {{Times New Roman|VII}}. Pour enrichir l'harmonie, on peut utiliser les dominantes secondaires, en particulier : * {{Times New Roman|V}} de {{Times New Roman|V}} ({{Times New Roman|II}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|V}}, * {{Times New Roman|V}} de {{Times New Roman|IV}} ({{Times New Roman|I}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|IV}}. On peut enchaîner les enchaînements, par exemple {{Times New Roman|I}} → {{Times New Roman|IV}} → {{Times New Roman|V}}, ou encore {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}}… En jazz, on utilise très fréquemment l'enchaînement {{Times New Roman|II}} → {{Times New Roman|V}} → {{Times New Roman|I}} (deux-cinq-un). On peut bien sûr avoir d'autres enchaînements, mais ces règles permettent d'analyser un grand nombre de morceaux, et donnent des clefs utiles pour la composition. Nous voyons ci-après un certain nombre d'enchaînements courants dans différents styles == Exercice == Un hautboïste travaille la sonate en ''do'' mineur S. 277 de Heinichen. Sur le deuxième mouvement ''Allegro'', il a du mal à travailler un passage en raison des altérations accidentelles. Sur la suggestion de sa professeure, il décide d'analyser la progression d'accords sous-jacente afin que les altérations deviennent logiques. Il s'agit d'un duo hautbois et basson pour lequel les accords ne sont pas chiffrés, le basson étant ici un instrument soliste et non pas un élément de la basse continue. Sur l'extrait suivant, déterminez les basses et la qualité (chiffrage) des accords sous-jacents. Commentez. [[Fichier:Sonate hautbois basson heinichen 2e mvt mes49.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen.]] {{note|L'œuvre est en ''do'' mineur et devrait donc avoir trois bémols à la clef, or ici il n'y en a que deux. En effet, le ''la'' pouvant être bécarre en mode mineur mélodique ascendant, le compositeur a préféré le noter explicitement en altération accidentelle lorsque l'on est en mode mélodique naturel, harmonique ou mélodique descendant. C'est un procédé assez courant à l'époque baroque.}} {{boîte déroulante/début|titre=Solution}} Une des difficultés ici est que les arpèges joués par les instruments sont agrémentés de notes de passage. Les notes de la basse (du basson) sont différentes entre le premier et le deuxième temps de chaque mesure et ne peuvent pas appartenir au même accord. On a donc un accord par temps. Sur le premier temps de chaque mesure, le basson joue une octave. La note concernée est donc la basse de chaque accord. Pour savoir s'il s'agit d'un accord à l'état fondamental ou d'un renversement, on regarde ce que joue le hautbois : dans un mouvement conjoint (succession d'intervalles de secondes), il est difficile de distinguer les notes de l'arpège des notes de passage, mais : les notes des grands intervalles font partie de l'accord. Ainsi, sur le premier temps de la première mesure (la basse est un ''mi''♭), on a une sixte descendante ''sol''-''si''♭ et, à la fin du temps, une tierce descendante ''sol''-''mi''♭. L'accord est donc ''mi''♭-''sol''-''si''♭, c'est un accord de quinte (accord parfait à l'état fondamental). À la fin du premier temps, le basson joue un ''do'', c'est donc une note étrangère. Sur le second temps de la première mesure, le basson joue une tierce ascendante ''fa''-''la''♭, la première note est la basse de l'accord et la seconde une des notes de l'accord. Le hautbois commence par une sixte descendante ''la''♭-''do'', l'accord est donc ''fa''-''la''♭-''do'', un accord de quinte (accord parfait à l'état fondamental). Le ''do'' du basson la fin du premier temps est donc une anticipation. Les autres notes étrangères de la première mesure sont des notes de passage. Mais il faut faire attention : en suivant ce principe, sur les premiers temps des deuxième et troisième mesure, nous aurions des accords de septième d'espèce (puisque la septième est majeure). Or, on ne trouve pas, ou alors exceptionnellement, d'accord de septième d'espèce dans le baroque, mais quasi exclusivement des accords de septième de dominante. Donc au début de la deuxième mesure, le ''la''♮ est une appoggiature du ''si''♭, l'accord est donc ''si''♭-''ré''-''fa'', un asscord de quinte. De même, au début de la troisième mesure, le ''sol'' est une appoggiature du ''la''♭. Il faut donc se méfier d'une analyse purement « mathématique ». Il faut s'attacher à ressentir la musique, et à connaître les styles, pour faire une analyse pertinente. Ci-dessous, nous avons grisé les notes étrangères. [[Fichier:Sonate hautbois basson heinichen 2e mvt mes49 analyse.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen. Analyse de la progression harmonique.]] Le chiffrage jazz équivalent est : : | E♭ F– | B♭<sup>Δ</sup> E♭ | A♭<sup>Δ</sup> D– | G … Nous remarquons une progression assez régulière : : ''mi''♭ ↗[2<sup>de</sup>] ''fa'' | ↘[5<sup>te</sup>] ''si''♭ ↗[4<sup>te</sup>] ''mi''♭ | ↘[5<sup>te</sup>] ''la''♭ ↗[4<sup>te</sup>] ''ré'' | ↘[5<sup>te</sup>] ''sol'' Le ''mi''♭ est le degré {{Times New Roman|III}} de la tonalité principale (''do'' mineur), c'est donc une tonique faible ; il « joue le même rôle » qu'un ''do''. S'il y avait eu un accord de ''do'' au début de l'extrait, on aurait eu une progression parfaitement régulière ↗[4<sup>te</sup>] ↘[5<sup>te</sup>]. Nous avons les modulations suivantes : * mesure 49 : ''do'' mineur naturel (le ''si''♭ n'est pas une sensible) avec un accord sur “{{Times New Roman|I}}” (tonique faible, {{Times New Roman|III}}, pour la première analyse, ou bien tonique forte, {{Times New Roman|I}}, pour la seconde) suivi d'un accord sur {{Times New Roman|IV}} ; * mesure 50 : ''si''♭ majeur avec un accord sur {{Times New Roman|I}} suivi d'un accord sur {{Times New Roman|IV}} ; * mesure 51 : ''la''♭ majeur avec un accord sur {{Times New Roman|I}}, et emprunt à ''do'' majeur avec un accord sur {{Times New Roman|II}} ({{Times New Roman|IV}} faible). On a donc une marche harmonique {{Times New Roman|I}} → {{Times New Roman|IV}} qui descend d'une seconde majeure (un ton) à chaque mesure (''do'' → ''si''♭ → ''la''♭), avec une exception sur la dernière mesure (modulation en cours de mesure et descente d'une seconde mineure au lieu de majeure). Ce passage est donc construit sur une régularité, une règle qui crée un effet d'attente — enchaînement {{Times New Roman|I}}<sup>5</sup> → {{Times New Roman|IV}}<sup>5</sup> avec une marche harmonique d'une seconde majeure descendante —, et des « surprises », des exceptions au début — ce n'est pas un accord {{Times New Roman|I}}<sup>5</sup> mais un accord {{Times New Roman|III}}<sup>5</sup> — et à la fin — modulation en milieu de mesure et dernière descente d'une seconde mineure (½t ''la''♭ → ''sol''). L'extrait ne permet pas de le deviner, mais la mesure 52 est un retour en ''do'' mineur, avec donc une modulation sur la dominante (accord de ''sol''<sup>7</sup><sub>+</sub>, G<sup>7</sup>). {{boîte déroulante/fin}} == Progression d'accords == Comme pour la mélodie, la succession des accords dans un morceau, la progression d'accords, suit des règles. Et comme pour la mélodie, les règles diffèrent d'un style musical à l'autre et la créativité consiste à parfois ne pas suivre ces règles. Et comme pour la mélodie, on part d'un ensemble de notes organisé, d'une gamme caractéristique d'une tonalité, d'un mode. Les accords les plus utilisés pour une tonalité donnée sont les accords dont la fondamentale sont les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la tonalité, en particulier la triade {{Times New Roman|I}}, appelée « accord parfait » ou « accord de tonique », et l'accord de septième {{Times New Roman|V}}, appelé « septième de dominante ». Le fait d'avoir une progression d'accords qui se répète permet de structurer un morceau. Pour les morceaux courts, il participe au plaisir de l'écoute et facilite la mémorisation (par exemple le découpage couplet-refrain d'une chanson). Sur les morceaux longs, une trop grande régularité peut introduire de la lassitude, les longs morceaux sont souvent découpés en parties présentant chacune une progression régulière. Le fait d'avoir une progression régulière permet la pratique de l'improvisation : cadence en musique classique, solo en jazz et blues. ; Note : Le terme « cadence » désigne plusieurs choses différentes, et notamment en harmonie : :* une partie improvisée dans un opéra ou un concerto, sens utilisé ci-dessus ; :* une progression d'accords pour ponctuer un morceau et en particulier pour le conclure, sens utilisé dans la section suivante. === Accords peu utilisés === En mode mineur, l'accord de quinte augmentée {{Times New Roman|III<sup>+5</sup>}} est très peu utilisé. C'est un accord dissonant ; il intervient en général comme appogiature de l'accord de tonique (par exemple en ''la'' mineur : {{Times New Roman|III<sup>+5</sup>}} ''do'' - ''mi'' - ''sol''♯ → {{Times New Roman|I<sup>6</sup>}} ''do'' - ''mi'' - ''la''), ou de l'accord de dominante ({{Times New Roman|III<sup>6</sup><sub>+3</sub>}} ''mi'' - ''sol''♯ - ''do'' → {{Times New Roman|V<sup>5</sup>}} ''mi'' - ''sol''♯ - ''si''). Il peut être aussi utilisé comme préparation à l'accord de sous-dominante (enchaînement {{Times New Roman|III}} → {{Times New Roman|IV}}). Par ailleurs, il a une constitution symétrique — c'est l'empilement de deux tierces majeures — et ses renversements ont les mêmes intervalles à l'enharmonie près (quinte augmentée/sixte mineure, tierce majeure/quarte diminuée). De ce fait, un même accord est commun, par renversement et à l'enharmonie près, à trois tonalités : le premier renversement de l'accord ''do'' - ''mi'' - ''sol''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur) est enharmonique à ''mi'' - ''sol''♯ - ''si''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''do''♯ mineur) ; le second renversement est enharmonique à ''la''♭ - ''do'' - ''mi'' ({{Times New Roman|III}}<sup>e</sup> degré de ''fa'' mineur). === Accords très utilisés === Les trois accords les plus utilisés sont les accords de tonique (degré {{Times New Roman|I}}), de sous-dominante ({{Times New Roman|IV}}) et de dominante ({{Times New Roman|V}}). Ils interviennent en particulier en fin de phrase, dans les cadences. L'accord de dominante sert souvent à introduire une modulation : la modulation commence sur l'accord de dominante de la nouvelle tonalité. On note que l'accord de sous-dominante est situé une quinte juste en dessous de la tonique, les accords de dominante et de sous-dominante sont donc symétriques. En jazz, on utilise également très fréquemment l'accord de la sus-tonique (degré {{Times New Roman|II}}), souvent dans des progressions {{Times New Roman|II}} - {{Times New Roman|V}} (- {{Times New Roman|I}}). Rappelons que l'accord de sus-tonique a la fonction de sous-dominante. === Cadences et ''turnaround'' === Le terme « cadence » provient de l'italien ''cadenza'' et désigne la « chute », la fin d'un morceau ou d'une phrase musicale. On distingue deux types de cadences : * les cadences conclusive, qui créent une sensation de complétude ; * les cadences suspensives, qui crèent une sensation d'attente. ==== Cadence parfaite ==== [[Fichier:Au clair de le lune cadence parfaite.midi|thumb|''Au clair de la lune'', harmonisé avec une cadence parfaite (italienne).]] [[Fichier:Au clair de le lune mineur cadence parfaite.midi|thumb|''Idem'' mais en mode mineur harmonique.]] La cadence parfaite est l'enchaînement de l'accord de dominante suivi de l'accord parfait : {{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}}, les deux accord étant à l'état fondamental. Elle donne une impression de stabilité et est donc très souvent utilisée pour conclure un morceau. C'est une cadence conclusive. On peut aussi utiliser l'accord de septième de dominante, la dissonance introduisant une tension résolue par l'accord parfait : {{Times New Roman|V<sup>7</sup><sub>+</sub> - I<sup>5</sup>}}. Elle est souvent précédée de l'accord construit sur le IV<sup>e</sup> degré, appelé « accord de préparation », pour former la cadence italienne : {{Times New Roman|IV<sup>5</sup> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}}. Elle est également souvent précédée du second renversement de l'accord de tonique, qui est alors appelé « appoggiature de la cadence » : {{Times New Roman|I<sup>6</sup><sub>4</sub> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}} (on remarque que les accords {{Times New Roman|I}}<sup>6</sup><sub>4</sub> et {{Times New Roman|V}}<sup>5</sup> ont la basse en commun, et que l'on peut passer de l'un à l'autre par un mouvement conjoint sur les autres notes). {{clear}} ==== Demi-cadence ==== [[Fichier:Au clair de le lune demi cadence.midi|thumb|''Au clair de la lune'', harmonisé avec une demi-cadence.]] Une demi-cadence est une phrase ou un morceau se concluant sur l'accord construit sur le cinquième degré. Il provoque une sensation d'attente, de suspens. Il s'agit en général d'une succession {{Times New Roman|II - V}} ou {{Times New Roman|IV - V}}. C'est une cadence suspensive. On utilise rarement un accord de septième de dominante. {{clear}} ==== Cadence rompue ou évitée ==== La cadence rompue, ou cadence évitée, est succession d'un accord de dominante et d'un accord de sus-dominante, {{Times New Roman|V}} - {{Times New Roman|VI}}. C'est une cadence suspensive. ==== Cadence imparfaite ==== Une cadence imparfaite est une cadence {{Times New Roman|V - I}}, comme la cadence parfaite, mais dont au moins un des deux accords est dans un état renversé. ==== Cadence plagale ==== La cadence plagale — du grec ''plagios'', oblique, en biais — est la succession de l'accord construit sur le quatrième degré, suivi de l'accord parfait : {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}. Elle peut être utilisée après une cadence parfaite ({{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}} - {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}). Elle donne un caractère solennel, voire religieux — elle est parfois appelée « cadence amen » —, elle a un côté antique qui rappelle la musique modale et médiévale<ref>{{lien web |url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/la-cadence-amen-ou-comment-se-dire-adieu-7191921 |titre=La cadence « Amen » ou comment se dire adieu |auteur=Max Dozolme (MAXXI Classique) |site=France Musique |date=2025-04-25 |consulté le=2025-04-25}}.</ref>. C'est une cadence conclusive. ==== {{lang|en|Turnaround}} ==== [[Fichier:Au clair de le lune turnaround.midi|thumb|Au clair de la lune, harmonisé en style jazz : accords de 7{{e}}, anatole suivie d'un ''{{lang|en|turnaround}}'' ii-V-I.]] Le terme ''{{lang|en|turnaround}}'' signifie revirement, retournement. C'est une succession d'accords que fait la transition entre deux parties, en créant une tension-résolution. Le ''{{lang|en|turnaround}}'' le plus courant est la succession {{Times New Roman|II - V - I}}. On utilise également fréquemment l'anatole : {{Times New Roman|I - VI - II - V}}. {| class="wikitable" |+ Progressions typiques d'accords dans une tonalité majeure ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Cadence<br />parfaite<br /> {{Times New Roman|V - I}} ! scope="col" | Cadence<br />italienne<br />{{Times New Roman|IV - V - I}} ! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou IV - V}} ! scope="col" | Cadence<br />plagale<br />{{Times New Roman|IV - I}} ! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}} ! scope="col" | Anatole<br />{{Times New Roman|I - vi - ii - V}} |- |''Do'' majeur || || G - C || F - G - C || Dm - G ou F - G || F - C || Dm - G - C || C - Am - Dm - G |- |''Sol'' majeur || ''fa''♯ || D - G || C - D - G || Am - D ou C - D || C - G || Am - D - G || G - Em - Am - D |- |''Ré'' majeur || ''fa''♯, ''do''♯ || A - D || G - A - D || Em - A ou G - A || G - D || Em - A - D || D - Bm - Em - A |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || E - A || D - E - A || Bm - E ou D - E || D - A || Bm - E - A || A - F♯m - B - E |- | ''Fa'' majeur || ''si''♭ || C - F || B♭ - C - F || Gm - C ou B♭ - C || B♭ - F || Gm - C - F || F - Dm - Gm - C |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || F - B♭ || E♭ - F - B♭ || Cm - F ou E♭ - F || E♭ - B♭ || Cm - F - B♭ || B♭ - Gm - Cm - F |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || B♭ - E♭ || A♭ - B♭ - E♭ || Fm - B♭ ou A♭ - B♭ || A♭ - E♭ || Fm - B♭ - E♭ || Gm - Cm - Fm - B♭ |} {| class="wikitable" |+ Progressions typiques d'accords dans une tonalité mineure ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Cadence<br />parfaite<br />{{Times New Roman|V - i}} ! scope="col" | Cadence<br />italienne<br />{{Times New Roman|iv - V - i}} ! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou iv - V}} ! scope="col" | Cadence<br />plagale<br />{{Times New Roman|iv - i}} ! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}} ! scope="col" | Anatole<br />{{Times New Roman|i - VI - ii - V}} |- | ''La'' mineur<br />harmonique || || E - Am || Dm - E - Am || B° - E ou Dm - E || Dm - Am || B° - E - Am || Am - F - B° - E |- | ''Mi'' mineur<br />harmonique || ''fa''♯ || B - Em || Am - B - Em || F♯° - B ou Am - B || Am - Em || F♯° - B - Em || Em - C - F♯° - B |- | ''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || F♯ - Bm || Em - F♯ - Bm || C♯° - F♯ ou Em - F♯ || Em - Bm || C♯° - F♯ - Bm || Bm - G - C♯° - F♯ |- | ''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || C♯ - F♯m || Bm - C♯ - F♯m || G♯° - C♯ ou Bm - C♯ || Bm - F♯m || G♯° - C♯ - F♯m || A+ - D - G♯° - C♯ |- | ''Ré'' mineur<br />harmonique || ''si''♭ || A - Dm || Gm - A - Dm || E° - A ou Gm - A || Gm - Dm || E° - A - Dm || Dm - B♭ - E° - A |- | ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || D - Gm || Cm - D - Gm || A° - D ou Cm - D || Cm - Gm|| A° - D - Gm || Gm - E♭ - A° - D |- | ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || G - Cm || Fm - G - Cm || D° - G ou Fm - G || Fm - Dm || D° - G - Cm || Cm - A♭ - D° - G |} ==== Exemple : ''La Mer'' ==== : {{lien web | url = https://www.youtube.com/watch?v=PXQh9jTwwoA | titre = Charles Trenet - La mer (Officiel) [Live Version] | site = YouTube | auteur = Charles Trenet | consulté le = 2020-12-24 }} Le début de ''La Mer'' (Charles Trenet, 1946) est en ''do'' majeur et est harmonisé par l'anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (C - Am - Dm - G<sup>7</sup>) sur deux mesures, jouée deux fois ({{Times New Roman|1=<nowiki>|I-vi|ii-V</nowiki><sup>7</sup><nowiki>|</nowiki>}} × 2). Viennent des variations avec les progressions {{Times New Roman|I-III-vi-V<sup>7</sup>}} (C - E - Am - G<sup>7</sup>) puis la « progression ’50s » (voir plus bas) {{Times New Roman|I-vi-IV-VI<sup>7</sup>}} (C - Am - F - A<sup>7</sup>, on remarque que {{Times New Roman|IV}}/F est le relatif majeur du {{Times New Roman|ii}}/Dm de l'anatole), jouées chacune une fois sur deux mesure ; puis cette première partie se conclut par une demie cadence {{Times New Roman|ii-V<sup>7</sup>}} sur une mesure puis une dernière anatole sur trois mesures ({{Times New Roman|1=<nowiki>|I-vi|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}}). Cela constitue une première partie « A » sur douze mesures qui se termine par une demi-cadence ({{Times New Roman|ii-V<sup>7</sup>}}) qui appelle une suite. Cette partie A est jouée une deuxième fois mais la fin est modifiée pour la transition : les deux dernières mesures {{Times New Roman|<nowiki>|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}} deviennent {{Times New Roman|<nowiki>|ii-V</nowiki><sup>7</sup><nowiki>|I|</nowiki>}} (|Dm-G7|C|), cette partie « A’ » se conclut donc par une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}). Le morceau passe ensuite en tonalité de ''mi'' majeur, donc une tierce au dessus de ''do'' majeur, sur six mesures. Cette partie utilise une progression ’50s {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (E - C♯m - A - B<sup>7</sup>), qui est rappelons-le une variation de l'anatole, l'accord {{Times New Roman|ii}} (Fm) étant remplacé par son relatif majeur {{Times New Roman|IV}} (A). Cette anatole modifiée est jouée deux fois puis la partie en ''mi'' majeur se conclut par l'accord parfait {{Times New Roman|I}} joué sur deux mesures (|E|E|), on a donc, avec la mesure précédente, avec une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}). Suivent ensuite six mesures en ''sol'' majeur, donc à nouveau une tierce au dessus de ''mi'' majeur. Elle comporte une progression {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (G - Em - C - D<sup>7</sup>), donc anatole avec substitution du {{Times New Roman|ii}}/Am par son relatif majeur {{Times New Roman|VI}}/C (progression ’50s), puis une anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (G - Em - Am - D<sup>7</sup>) et deux mesure sur la tonique {{Times New Roman|I-I<sup>7</sup>}} (G - G<sup>7</sup>), formant à nouveau une cadence parfaite. La fin sur un accord de septième, dissonant, appelle une suite. Cette partie « B » de douze mesures comporte donc deux parties similaires « B1 » et « B2 » qui forment une marche harmonique (montée d'une tierce). Le morceau se conclut par une reprise de la partie « A’ » et se termine donc par une cadence parfaite. Nous avons une structure A-A’-B-A’ sur 48 mesures, proche la forme AABA étudiée plus loin. Donc ''La Mer'' est un morceau structuré autour de l'anatole avec des variations (progression ’50s, substitution du {{Times New Roman|ii}} par son relatif majeur {{Times New Roman|IV}}) et comportant une marche harmonique dans sa troisième partie. Les parties se concluent par des ''{{lang|en|turnarounds}}'' sous la forme d'une cadence parfaite ou, pour la partie A, par une demi-cadence. {| border="1" rules="rows" frame="hsides" |+ Structure de ''La Mer'' |- align="center" | | colspan="12" | ''do'' majeur | |- align="center" ! scope="row" rowspan=2 | A | colspan="2" | anatole | colspan="2" | // | colspan="2" | variation | colspan="2" | ’50s | ½ c. | colspan="3" | anatole | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii}} || <nowiki>|</nowiki> {{Times New Roman|V<sup>7</sup>}} || <nowiki>|</nowiki> |- align="center" ! scope="row" rowspan="2" | A’ | colspan="2" | anatole | colspan="2" | // | colspan="2" | variation | colspan="2" | ’50s | ½ c. | colspan="2" | anatole | c.p. | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> |- align="center" | | colspan="6" | B1 : ''mi'' majeur | colspan="6" background="lightgray" | B2 : ''sol'' majeur | |- align="center" ! scope="row" rowspan="2" | B | colspan="2" | ’50s | colspan="2" | // |colspan="2" | c.p. | colspan="2" | ’50s | colspan="2" | // |colspan="2" | c.p. | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I<sup>7</sup>}} || <nowiki>|</nowiki> |- ! scope="row" | A’ | colspan="12" | | |} === Progression blues === La musique blues est apparue dans les années 1860. Elle est en général bâtie sur une grille d'accords ''({{lang|en|changes}})'' immuable de douze mesures ''({{lang|en|twelve-bar blues}})''. C'est sur cet accompagnement qui se répète que s'ajoute la mélodie — chant et solo. Cette structure est typique du blues et se retrouve dans ses dérivés comme le rock 'n' roll. Le rythme est toujours un rythme ternaire syncopé ''({{lang|en|shuffle, swing, groove}}, ''notes inégales'')'' : la mesure est à quatre temps, mais la noire est divisée en noire-croche en triolet, ou encore triolet de croche en appuyant la première et la troisième. La mélodie se construit en général sur une gamme blues de six degrés (gamme pentatonique mineure avec une quarte augmentée), mais bien que la gamme soit mineure, l'harmonie est construite sur la gamme majeure homonyme : un blues en ''fa'' a une mélodie sur la gamme de ''fa'' mineur, mais une harmonie sur la gamme de ''fa'' majeur. La grille d'accord comporte les accords construits sur les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la gamme majeure homonyme. Les accords sont souvent des accords de septième (donc avec une tierce majeure et une septième mineure), il ne s'agit donc pas d'une harmonisation de gamme diatonique (puisque la septième est majeure sur l'accord de tonique). Par exemple, pour un blues en ''do'' : * accord parfait de do majeur, C ({{Times New Roman|I}}<sup>er</sup> degré) ; * accord parfait de fa majeur, F ({{Times New Roman|IV}}<sup>e</sup> degré) ; * accord parfait de sol majeur, G ({{Times New Roman|V}}<sup>e</sup> degré). Il existe quelques morceaux harmonisés avec des accords mineurs, comme par exemple ''As the Years Go Passing By'' d'Albert King (Duje Records, 1959). La progression blues est organisée en trois blocs de quatre mesures ayant les fonctions suivantes (voir ci-dessus ''[[#Harmonie fonctionnelle|Harmonie fonctionnelle]]'') : * quatre mesures toniques ; * quatre mesures sous-dominantes ; * quatre mesures dominantes. La forme la plus simple, que Jeff Gardner appelle « forme A », est la suivante : {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, forme A |- ! scope="row" | Tonique | width="50px" | I | width="50px" | I | width="50px" | I | width="50px" | I |- ! scope="row" | Sous-domminante | width="50px" | IV | width="50px" | IV | width="50px" | I | width="50px" | I |- ! scope="row" | Dominante | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | V |} La progression {{Times New Roman|I-V}} des deux dernières mesures forment le ''{{lang|en|turnaround}}'', la demie cadence qui lance le cycle suivant. Nous présentons ci-dessous un exemple typique de ligne de basse ''({{lang|en|walking bass}})'' pour le ''{{lang|en|turnaround}}'' d'un blues en ''la'' : [[Fichier:Turnaround classique blues en la.svg|Exemple typique de ligne de basse pour un ''turnaround'' de blues en ''la''.]] [[Fichier:Blues mi harmonie elementaire.midi|thumb|Blues en ''mi'', harmonisé de manière élémentaire avec une ''{{lang|en|walking bass}}''.]] Vous pouvez écouter ci-contre une harmonisation typique d'un blues en ''mi''. Les accords sont exécutés par une basse marchante ''({{lang|en|walking bass}})'', qui joue une arpège sur la triade avec l'ajout d'une sixte majeure et d'une septième mineure, et par une guitare qui joue un accord de puissance ''({{lang|en|power chord}})'', qui n'est composé que de la fondamentale et de la quinte juste, avec une sixte en appoggiature. La forme B s'obtient en changeant la deuxième mesure : on joue un degré {{Times New Roman|IV}} au lieu d'un degré {{Times New Roman|I}}. La progression {{Times New Roman|I-IV}} sur les deux premières mesures est appelé ''{{lang|en|quick change}}''. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, forme B |- | width="50px" | I | width="50px" | IV | width="50px" | I | width="50px" | I |- | width="50px" | IV | width="50px" | IV | width="50px" | I | width="50px" | I |- | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | V |} Par exemple, ''Sweet Home Chicago'' (Robert Johnson, 1936) est un blues en ''fa'' ; sa grille d'accords, aux variations près, suit une forme B : {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression de ''Sweet Home Chicago'' |- | width="50px" | F | width="50px" | B♭ | width="50px" | F | width="50px" | F |- | width="50px" | B♭ | width="50px" | B♭ | width="50px" | F | width="50px" | F |- | width="50px" | C7 | width="50px" | B♭7 | width="50px" | F7 | width="50px" | C7 |} : Écouter {{lien web | url =https://www.youtube.com/watch?v=dkftesK2dck | titre = Robert Johnson "Sweet Home Chicago" | auteur = Michal Angel | site = YouTube | date = 2007-12-09 | consulté le = 2020-12-17 }} Les formes C et D s'obtiennent à partir des formes A et B en changeant le dernier accord par un accord sur le degré {{Times New Roman|I}}, ce qui forme une cadence plagale. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, formes C et D |- | colspan="4" | … |- | colspan="4" | … |- | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | I |} L'harmonie peut être enrichie, notamment en jazz. Voici par exemple une grille du blues souvent utilisés en bebop. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Exemple de progression de blues bebop sur une base de forme B |- | width="60px" | I<sup>7</sup> | width="60px" | IV<sup>7</sup> | width="60px" | I<sup>7</sup> | width="60px" | V–<sup>7</sup> <nowiki>|</nowiki> I<sup>7</sup> |- | width="60px" | IV<sup>7</sup> | width="60px" | IV<sup>7</sup> | width="60px" | I<sup>7</sup> | width="60px" | VI<sup>7 ♯9 ♭13</sup> |- | width="60px" | II–<sup>7</sup> | width="60px" | V<sup>7</sup> | width="60px" | V<sup>7</sup> <nowiki>|</nowiki> IV<sup>7</sup> | width="60px" | II–<sup>7</sup> <nowiki>|</nowiki> V<sup>7</sup> |} On peut aussi trouver des blues sur huit mesures, sur seize mesures comme ''Watermelon Man'' de Herbie Hancock (album ''Takin' Off'', Blue Note, 1962) ou ''Let's Dance'' de Jim Lee (interprété par Chris Montez, Monogram, 1962) * {{lien web |url= https://www.dailymotion.com/video/x5iduwo |titre=Herbie Hancock - Watermelon Man (1962) |auteur=theUnforgettablesTv |site=Dailymotion |date=2003 |consulté le=2021-02-09 }} * {{lien web |url=https://www.youtube.com/watch?v=6JXshurYONc |titre=Let's Dance |auteur=Chris Montez |site=YouTube |date=2016-08-06 |consulté le=2021-02-09 }} À l'inverse, certains blues peuvent avoir une structure plus simple que les douze mesure ; par exemple ''Hoochie Coochie Man'' de Willie Dixon (interprété par Muddy Waters sous le titre ''Mannish Boy'', Chicago Blues, 1954) est construit sur un seul accord répété tout le long de la chanson. * {{lien web |url=https://www.dailymotion.com/video/x5iduwo |titre=Muddy Waters - Hoochie Coochie Man |auteur=Muddy Waters |site=Dailymotion |date=2012 | consulté le=2021-02-09 }} === Cadence andalouse === La cadence andalouse est une progression de quatre accords, descendant par mouvement conjoint : * en mode de ''mi'' (mode phrygien) : {{Times New Roman|IV}} - {{Times New Roman|III}} - {{Times New Roman|II}} - {{Times New Roman|I}} ;<br />par exemple en ''mi'' phrygien : Am - G - F - E ; en ''do'' phrygien : Fm - E♭ - D♭ - C ;<br />on notera que le degré {{Times New Roman|III}} est diésé dans l'accord final (ou bécarre s'il est bémol dans la tonalité) ; * en mode mineur : {{Times New Roman|I}} - {{Times New Roman|VII}} - {{Times New Roman|VI}} - {{Times New Roman|V}} ;<br />par exemple en ''la'' mineur : Am - G - F - E ; en ''do'' mineur : Cm - B♭ - A♭ - m ;<br />comme précédemment, on notera que le degré {{Times New Roman|VII}} est diésé dans l'accord final. === Progressions selon le cercle des quintes === [[Fichier:Cercle quintes degres tonalite majeure.svg|vignette|Cercle des quinte justes (parcouru dans le sens des aiguilles d'une montre) des degrés d'une tonalité majeure.]] La progression {{Times New Roman|V-I}} est la cadence parfaite, mais on peut aussi l'employer au milieu d'un morceau. Cette progression étant courte, sa répétition crée de la lassitude ; on peut la compléter par d'autres accords séparés d'une quinte juste, en suivant le « cercle des quintes » : {{Times New Roman|I-V-IX}}, la neuvième étant enharmonique de la seconde, on obtient {{Times New Roman|I-V-II}} ; en retournant la progression, on obtient le turnaround {{Times New Roman|II-V-I}}. En jazz, dans le morceau ''Topsy'' (Edgar Battle et Eddir Durham), la partie <span style="border-color: black; border-style: solid; border-width: thin;">C</span> est composée d'une progression en quintes descendantes d'accords de septième de dominante : D<sup>7</sup> - G<sup>7</sup> - C<sup>7</sup> - F<sup>7</sup> - B♭<sup>7</sup> ; les improvisatiosn sur cette partie peuvent donc se faire en modulant en ''sol'' (majeur ou mineur) - ''do'' (M/m) - ''fa'' (M/m) - ''si''♭ (M/m) - ''mi''♭ (M/m). On peut continuer de décrire le cercle des quintes : {{Times New Roman|I-V-II-VI}}, on obtient l'anatole dans le désordre ; on peut à l'inverse étendre les quintes vers la gauche, {{Times New Roman|IV-I-V-II-VI}}. En musique populaire, on trouve fréquemment une progression fondée sur les accord {{Times New Roman|I}}, {{Times New Roman|IV}}, {{Times New Roman|V}} et {{Times New Roman|VI}}, popularisée dans les années 1950. La « progression années 1950 », « progression ''{{lang|en|fifties ('50)}}'' » ''({{lang|en|'50s progression}})'' est dans l'ordre {{Times New Roman|I-VI-IV-V}}. On trouve aussi cette progression en musique classique. Si la tonalité est majeure, la triade sur la sus-dominante est mineure, les autres sont majeures, on notera donc souvent {{Times New Roman|I-vi-IV-V}}. On peut avoir des permutations circulaires (le dernier accord venant au début, ou vice-versa) : {{Times New Roman|vi-IV-V-I}}, {{Times New Roman|IV-V-I-vi}} et {{Times New Roman|V-I-vi-IV}}. {| class="wikitable" |+ Accords selon la tonalité ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" style="font-family:Times New Roman" | I ! scope="col" style="font-family:Times New Roman" | IV ! scope="col" style="font-family:Times New Roman" | V ! scope="col" style="font-family:Times New Roman" | vi |- |''Do'' majeur || || C || F || G || Am |- |''Sol'' majeur || ''fa''♯ || G || C || D || Em |- |''Ré'' majeur || ''fa''♯, ''do''♯ || D || G || A || Bm |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A || D || E || F♯m |- | ''Fa'' majeur || ''si''♭ || F || B♭ || C || Dm |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭ || E♭ || F || Gm |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭ || A♭ || B♭ || Cm |} Par exemple, en tonalité de ''do'' majeur, la progression {{Times New Roman|I-vi-IV-V}} sera C-Am-F-G. Il existe d'autres progressions utilisant ces accords mais dans un autre ordre, typiquement {{Times New Roman|I–IV–vi–V}} ou une de ses permutations circulaires : {{Times New Roman|IV–vi–V-I}}, {{Times New Roman|vi–V-I-IV}} ou {{Times New Roman|V-I-IV-vi}}. Ou dans un autre ordre. PV Nova l'illustre dans plusieurs de ses « expériences » dans la version {{Times New Roman|vi-V-IV-I}}, soit Am-G-F-C, ou encore {{Times New Roman|vi-IV-I-V}}, soit Am-F-C-G : : {{lien web | url = https://www.youtube.com/watch?v=w08LeZGbXq4 | titre = Expérience n<sup>o</sup> 6 — La Happy Pop | auteur = PV Nova | site = YouTube | date = 2011-08-20 | consulté le = 2020-12-13 }} et cela devient un gag récurrent avec son « chapeau des accords magiques qu'on nous ressort à toutes les sauces » : {{lien web | url = https://www.youtube.com/watch?v=VMY_vc4nZAU | titre = Expérience n<sup>o</sup> 14 — La Soupe dou Brasil | auteur = PV Nova | site = YouTube | date = 2012-10-03 | consulté le = 2020-12-17 }} Cette récurrence est également parodiée par le groupe The Axis of Awesome avec ses « chansons à quatre accords » ''({{lang|en|four-chords song}})'', dans une sketch où ils mêlent 47 chansons en utilisant l'ordre {{Times New Roman|I-V-vi-IV}} : : {{lien web | url = https://www.youtube.com/watch?v=oOlDewpCfZQ | titre = 4 Chords | Music Videos | The Axis Of Awesome | auteur = The Axis of Awesome | site = YouTube | date = 2011-07-20 | consulté le = 2020-12-17 }} {{boîte déroulante/début|titre=Chansons mêlées dans le sketch}} # Journey : ''Don't Stop Believing'' ; # James Blunt : ''You're Beautiful'' ; # Black Eyed Peas : ''Where Is the Love'' ; # Alphaville : ''Forever Young'' ; # Jason Mraz : ''I'm Yours'' ; # Train : ''Hey Soul Sister'' ; # The Calling : ''Wherever You Will Go'' ; # Elton John : ''Can You Feel The Love Tonight'' (''Le Roi lion'') ; # Akon : ''Don't Matter'' ; # John Denver : ''Take Me Home, Country Roads'' ; # Lady Gaga : ''Paparazzi'' ; # U2 : ''With Or Without You'' ; # The Last Goodnight : ''Pictures of You'' ; # Maroon Five : ''She Will Be Loved'' ; # The Beatles : ''Let It Be'' ; # Bob Marley : ''No Woman No Cry'' ; # Marcy Playground : ''Sex and Candy'' ; # Men At Work : ''Land Down Under'' ; # thème de ''America's Funniest Home Videos'' (équivalent des émissions ''Vidéo Gag'' et ''Drôle de vidéo'') ; # Jack Johnson : ''Taylor'' ; # Spice Girls : ''Two Become One'' ; # A Ha : ''Take On Me'' ; # Green Day : ''When I Come Around'' ; # Eagle Eye Cherry : ''Save Tonight'' ; # Toto : ''Africa'' ; # Beyonce : ''If I Were A Boy'' ; # Kelly Clarkson : ''Behind These Hazel Eyes'' ; # Jason DeRulo : ''In My Head'' ; # The Smashing Pumpkins : ''Bullet With Butterfly Wings'' ; # Joan Osborne : ''One Of Us'' ; # Avril Lavigne : ''Complicated'' ; # The Offspring : ''Self Esteem'' ; # The Offspring : ''You're Gonna Go Far Kid'' ; # Akon : ''Beautiful'' ; # Timberland featuring OneRepublic : ''Apologize'' ; # Eminem featuring Rihanna : ''Love the Way You Lie'' ; # Bon Jovi : ''It's My Life'' ; # Lady Gaga : ''Pokerface'' ; # Aqua : ''Barbie Girl'' ; # Red Hot Chili Peppers : ''Otherside'' ; # The Gregory Brothers : ''Double Rainbow'' ; # MGMT : ''Kids'' ; # Andrea Bocelli : ''Time To Say Goodbye'' ; # Robert Burns : ''Auld Lang Syne'' ; # Five for fighting : ''Superman'' ; # The Axis of Awesome : ''Birdplane'' ; # Missy Higgins : ''Scar''. {{boîte déroulante/fin}} Vous pouvez par exemple jouer les accords C-G-Am-F ({{Times New Roman|I-V-vi-IV}}) et chanter dessus ''{{lang|en|Let It Be}}'' (Paul McCartney, The Beattles, 1970) ou ''Libérée, délivrée'' (Robert Lopez, ''La Reine des neiges'', 2013). La progression {{Times New Roman|I-V-vi-IV}} est considérée comme « optimiste » tandis que sa variante {{Times New Roman|iv-IV-I-V}} est considérée comme « pessimiste ». On peut voir la progression {{Times New Roman|I-vi-IV-V}} comme une variante de l'anatole {{Times New Roman|I-vi-ii-V}}, obtenue en remplaçant l'accord de sustonique {{Times New Roman|ii}} par l'accord de sous-dominante {{Times New Roman|IV}} (son relatif majeur, et degré ayant la même fonction). ==== Exemples de progression selon le cercle des quintes en musique classique ==== [[Fichier:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.midi|vignette|Dietrich Buxtehude, Psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]] Cette progression selon la cercle des quintes, sous la forme {{Times New Roman|I-vi-IV-V}}, apparaît déjà au {{pc|xvii}}<sup>e</sup> siècle dans le psaume 42 ''Quem ad modum desiderat cervis'' (BuxVW92) de Dietrich Buxtehude (1637-1707). Le morceau est en ''fa'' majeur, la progression d'accords est donc F-Dm-B♭-C. : {{lien web | url = https://www.youtube.com/watch?v=8FmV9l1RqSg | titre = D. Buxtehude - Quemadmodum desiderat cervus, BuxWV 92 | auteur = Longobardo | site = YouTube | date = 2013-04-06 | consulté la = 2021-01-01 }} [[File:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.svg|vignette|450x450px|center|Dietrich Buxtehude, psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]] {{clear}} [[Fichier:JSBach BWV140 cantate 4 mesures.midi|vignette|J.-S. Bach, cantate BWV140, quatre premières mesures.]] On la trouve également dans l'ouverture de la cantate ''{{lang|de|Wachet auf, ruft uns die Stimme}}'' de Jean-Sébastien Bach (BWV140, 1731). Le morceau est en ''mi''♭ majeur, la progression d'accords est donc E♭-Cm-A♭<sup>6</sup>-B♭. [[Fichier:JSBach BWV140 cantate 4 mesures.svg|vignette|center|J.-S. Bach, cantate BWV140, quatre premières mesures.|alt=|517x517px]] {{clear}} [[Fichier:Mozart K310 Sonate8 mesures 45 a 49.midi|vignette|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]] La même progression est utilisée par Mozart, par exemple dans le premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778), la progression d'accords est C-Am-F-G qui correspond à la progression {{Times New Roman|III-i-VI-VII}} de ''la'' mineur, mais à la progression {{Times New Roman|I-vi-IV-V}} de la gamme relative, ''do'' majeur . [[Fichier:Mozart K310 Sonate8 mesures 45 a 49.svg|vignette|center|500px|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]] === Substitution tritonique === Un des accords les plus utilisés est donc l'accord de septième de dominante, {{Times New Roman|V<sup>7</sup><sub>+</sub>}} qui contient les degrés {{Times New Roman|V}}, {{Times New Roman|VII}}, {{Times New Roman|II}} ({{Times New Roman|IX}}) et {{Times New Roman|IV}}({{Times New Roman|XI}}) ; par exemple, en tonalité de ''do'' majeur, l'accord de ''sol'' septième (G<sup>7</sup>) contient les notes ''sol''-''si''-''ré''-''fa''. Si l'on prend l'accord dont la fondamentale est trois tons (triton) au-dessus ou en dessous — l'octave contenant six tons, on arrive sur la même note —, {{Times New Roman|♭II<sup>7</sup>}}, ici ''ré''♭ septième (D♭<sup>7</sup>), celui-ci contient les notes ''ré''♭-''fa''-''la''♭-''do''♭, cette dernière note étant l'enharmonique de ''si''. Les deux accords G<sup>7</sup> et D♭<sup>7</sup> ont donc deux notes en commun : le ''fa'' et le ''si''/''do''♭. Il est donc fréquent en jazz de substituer l'accord {{Times New Roman|V<sup>7</sup><sub>+</sub>}} par l'accord {{Times New Roman|♭II<sup>7</sup>}}. Par exemple, la progression {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|V<sup>7</sup>}}-{{Times New Roman|I<sup>&Delta;</sup>}} devient {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|♭II<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}}. C'est un procédé courant de réharmonisation (le fait de remplacer un accord par un autre dans un morceau existant). Les six substitutions possibles sont donc : C<sup>7</sup>↔F♯<sup>7</sup> - D♭<sup>7</sup>↔G<sup>7</sup> - D<sup>7</sup>↔A♭<sup>7</sup> - E♭<sup>7</sup>↔A<sup>7</sup> - E<sup>7</sup>↔B♭<sup>7</sup> - F<sup>7</sup>↔B<sup>7</sup>. [[Fichier:Übermäsiger Terzquartakkord.jpg|vignette|Exemple de cadence parfaite en ''do'' majeur avec substitution tritonique (sixte française).]] Dans l'accord D♭<sup>7</sup>, si l'on remplace le ''do''♭ par son ''si'' enharmonique, on obtient un accord de sixte augmentée : ''ré''♭-''fa''-''la''♭-''si''. Cet accord est utilisé en musique classique depuis la Renaissance ; on distingue en fait trois accords de sixte augmentée : * sixte française ''ré''♭-''fa''-''sol''-''si'' ; * sixte allemande : ''ré''♭-''fa''-''la''♭-''si'' ; * sixte italienne : ''ré''♭-''fa''-''si''. Par exemple, le ''Quintuor en ''ut'' majeur'' de Franz Schubert (1828) se termine par une cadence parfaite dont l'accord de dominante est remplacé par une sixte française ''ré''♭-''fa''-''si''-''sol''-''si'' (''ré''♭ aux violoncelles, ''fa'' à l'alto, ''si''-''sol'' aux seconds violons et ''si'' au premier violon). [[Fichier:Schubert C major Quintet ending.wav|vignette|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]] [[Fichier:Schubert C major Quintet ending.png|vignette|center|upright=2.5|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]] === Autres accords de substitution === Substituer un accord consiste à utiliser un accord provenant d'une tonalité étrangère à la tonalité en cours. À la différence d'une modulation, la substitution est très courte et ne donne pas l'impression de changer de tonalité ; on a juste un sentiment « étrange » passager. Un court passage dans une autre tonalité est également appelée « emprunt ». Nous avons déjà vu plusieurs méthodes de substitution : * utilisation d'une note étrangère : une note étrangère — note de passage, appoggiature, anticipation, retard… — crée momentanément un accord hors tonalité ; en musique classique, ceci n'est pas considéré comme un accord en propre, mais en jazz, on parle « d'accord de passage » et « d'accord suspendu » ; * utilisation d'une dominante secondaire : l'accord de dominante secondaire est hors tonalité ; le but ici est de faire une cadence parfaite, mais sur un autre degré que la tonique de la tonalité en cours ; * la substitution tritonique, vue ci-dessus, pour remplacer un accord de septième de dominante. Une dernière méthode consiste à remplacer un accord par un accord d'une gamme de même tonique, mais d'un autre mode ; on « emprunte » ''({{lang|en|borrow}})'' l'accord d'un autre mode. Par exemple, substituer un accord de la tonalité de ''do'' majeur par un accord de la tonalité de ''do'' mineur ou de ''do'' mode de ''mi'' (phrygien). Donc en ''do'' majeur, on peut remplacer un accord de ''ré'' mineur septième (D<sub>m</sub><sup>7</sup>) par un accord de ''ré'' demi-diminué (D<sup>⌀</sup>, D<sub>m</sub><sup>7&flat;5</sup>) qui est un accord appartenant à la donalité de ''la'' mineur harmonique. === Forme AABA === La forme AABA est composée de deux progressions de huit mesures, notées A et B ; cela représente trente-deux mesures au total, on parle donc souvent en anglais de la ''{{lang|en|32-bars form}}''. C'est une forme que l'on retrouve dans de nombreuses chanson de comédies musicales de Broadway comme ''Have You Met Miss Jones'' (''{{lang|en|I'd Rather Be Right}}'', 1937), ''{{lang|en|Over the Rainbow}}'' (''Le Magicien d'Oz'', Harold Harlen, 1939), ''{{lang|en|All the Things You Are}}'' (''{{lang|en|Very Warm for may}}'', 1939). Par exemple, la version de ''{{lang|en|Over the Rainbow}}'' chantée par Judy Garland est en ''la''♭ majeur et la progression d'accords est globalement : * A (couplet) : A♭-Fm | Cm-A♭ | D♭ | Cm-A♭ | D♭ | D♭-F | B♭-E♭ | A♭ * B (pont) : A♭ | B♭m | Cm | D♭ | A♭ | B♭-G | Cm-G | B♭m-E♭ soit en degrés : * A : {{Times New Roman|<nowiki>I-vi | iii-I | IV | iii-IV | IV | IV-vi | II-V | I</nowiki>}} * B : {{Times New Roman|<nowiki>I | ii | iii | IV | I | II-VII | iii-VII | ii-V</nowiki>}} Par rapport aux paroles de la chanson, on a * A : couplet 1 ''« {{lang|en|Somewhere […] lullaby}} »'' ; * A : couplet 2 ''« {{lang|en|Somewhere […] really do come true}} »'' ; * B : pont ''« {{lang|en|Someday […] you'll find me}} »'' ; * A : couplet 3 ''« {{lang|en|Somewhere […] oh why can't I?}} »'' ; : {{lien web | url = https://www.youtube.com/watch?v=1HRa4X07jdE | titre = Judy Garland - Over The Rainbow (Subtitles) | site = YouTube | auteur = Overtherainbow | consulté le = 2020-12-17 }} Une mise en œuvre de la forme AABA couramment utilisée en jazz est la forme anatole (à le pas confondre avec la succession d'accords du même nom), en anglais ''{{lang|en|rythm changes}}'' car elle s'inspire du morceau ''{{lang|en|I Got the Rythm}}'' de George Gerschwin (''Girl Crazy'', 1930) : * A : {{Times New Roman|I–vi–ii–V}} (succession d'accords « anatole ») ; * B : {{Times New Roman|III<sup>7</sup>–VI<sup>7</sup>–II<sup>7</sup>–V<sup>7</sup>}} (les fondamentales forment une succession de quartes, donc parcourent le « cercle des quintes » à l'envers). Par exemple, ''I Got the Rythm'' étant en ''ré''♭ majeur, la forme est : * A : D♭ - B♭m - E♭m - A♭ * B : F7 - B♭7 - E♭7 - A♭7 === Exemples === ==== Début du Largo de la symphonie du Nouveau Monde ==== [[File:Largo nouveau monde 5 1res mesures.svg|vignette|Partition avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]] [[File:Largo nouveau monde 5 1res mesures.midi|vignette|Fichier son avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]] Nous avons reproduit ci-contre les cinq premières mesure du deuxième mouvement Largo de la symphonie « Du Nouveau Monde » (symphonie n<sup>o</sup> 9 d'Antonín Dvořák, 1893). Cliquez sur l'image pour l'agrandir. Vous pouvez écouter cette partie jouée par un orchestre symphonique : * {{lien web |url =https://www.youtube.com/watch?v=y2Nw9r-F_yQ?t=565 |titre = Dvorak Symphony No.9 "From the New World" Karajan 1966 |site=YouTube (Seokjin Yoon) |consulté le=2020-12-11 }} (à 9 min 25), par le Berliner Philharmoniker, dirigé par Herbert von Karajan (1966) ; * {{lien web |url = https://www.youtube.com/watch?v=ASlch7R1Zvo |titre=Dvořák: Symphony №9, "From The New World" - II - Largo |site=YouTube (diesillamusicae) |consulté le=2020-12-11 }} : Wiener Philharmoniker, dirigé par Herbert von Karajan (1985). {{clear}} Cette partie fait intervenir onze instruments monodiques (ne jouant qu'une note à la fois) : des vents (trois bois, sept cuivres) et une percussion. Certains de ces instruments sont transpositeurs (les notes sur la partition ne sont pas les notes entendues). Jouées ensemble, ces onze lignes mélodiques forment des accords. Pour étudier cette partition, nous réécrivons les parties des instruments transpositeurs en ''do'' et les parties en clef d’''ut'' en clef de ''fa''. Nous regroupons les parties en clef de ''fa'' d'un côté et les parties en clef de ''sol'' d'un autre. {{boîte déroulante|Résultat|contenu=[[File:Largo nouveau monde 5 1res mesures transpositeurs en do.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde, en do.]]}} Nous pouvons alors tout regrouper sous la forme d'un système de deux portées clef de ''fa'' et clef de ''sol'', comme une partition de piano. {{boîte déroulante|Résultat|contenu= [[Fichier:Largo nouveau monde 5 1res mesures accords.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords.]] }} [[Fichier:Largo nouveau monde 5 1res mesures accords.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords.]] {{clear}} Ensuite, nous ne gardons que la basse et les notes médium. Nous changeons éventuellement certaines notes d'octave afin de n'avoir que des superpositions de tierce ou de quinte (état fondamental des accords, en faisant ressortir les notes manquantes). {{boîte déroulante|Résultat|contenu= [[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords simplifiés.]] }} [[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords simplifiés.]] Vous pouvez écouter cette partie jouée par un quintuor de cuivres (trompette, bugle, cor, trombone, tuba), donc avec des accords de cinq notes : : {{lien web |url=https://www.youtube.com/watch?v=pWfe60nbvjA |titre = Largo from The New World Symphony by Dvorak |site=YouTube (The Chamberlain Brass) |consulté le=2020-12-11 }} : The American Academy of Arts & Letters in New York City (2017). Nous allons maintenant chiffrer les accords. Pour établir la basse chiffrée, il nous faut déterminer le parcours harmonique. Pour le premier accord, les tonalités les plus simples avec un ''sol'' dièse sont ''la'' majeur et ''fa'' dièse mineur ; comme le ''mi'' est bécarre, nous retenons ''la'' majeur, il s'agit donc d'un accord de quinte sur la dominante (les accords de dominante étant très utilisés, cela nous conforte dans notre choix). Puis nous avons un ''si'' bémol, nous pouvons être en ''fa'' majeur ou en ''ré'' mineur ; nous retenons ''fa'' majeur, c'est donc le renversement d'un accord sur le degré {{Times New Roman|II}}. Dans la deuxième mesure, nous revenons en ''la'' majeur, puis, avec un ''la'' et un ''ré'' bémols, nous sommes en ''la'' bémol majeur ; nous avons donc un accord de neuvième incomplet sur la sensible, ou un accord de onzième incomplet sur la dominante. Dans la troisième mesure, nous passons en ''ré'' majeur, avec un accord de dominante. Puis, nous arrivons dans la tonalité principale, avec le renversement d'un accord de dominante sans tierce suivi d'un accord de tonique. Nous avons donc une cadence parfaite, conclusion logique d'une phrase. La progression des accords est donc : {| class="wikitable" ! scope="row" | Tonalité | ''la'' M - ''fa'' M || ''la'' M - ''la''♭ M || ''ré'' M - ''ré''♭ M || ''ré''♭ M |- ! scope="row" | Accords | {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|II}}<sup>6</sup><sub>4</sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|“V”}}<sup>9</sup><sub><s>5</s></sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|V}}<sup>+4</sup> || {{Times New Roman|I}}<sup>5</sup> |} Dans le chiffrage jazz, nous avons donc : * une triade de ''mi'' majeur, E ; * une triade de ''sol'' majeur avec un ''ré'' en basse : G/D ; * à nouveau un E ; * un accord de ''sol'' neuvième diminué incomplet, avec un ''ré'' bémol en basse : G dim<sup>9</sup>/D♭ ; * un accord de ''la'' majeur, A ; * un accord de ''la'' bémol septième avec une ''sol'' bémol à la basse : A♭<sup>7</sup>/G♭ ; * la partie se conclue par un accord parfait de ''ré''♭ majeur, D♭. Soit une progression E - G/D | E - G dim<sup>9</sup>/D♭ | A - A♭<sup>7</sup>/G♭ | D♭. [[Fichier:Largo nouveau monde 5 1res mesures accords chiffres.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde en accords simplifiés.]] {{clear}} ==== Thème de Smoke on the Water ==== Le morceau ''Smoke on the Water'' du groupe Deep Purple (album ''Machine Head'', 1972) possède un célèbre thème, un riff ''({{lang|en|rythmic figure}})'', joué à la guitare sous forme d'accords de puissance ''({{lang|en|power chords}})'', c'est-à-dire des accords sans tierce. Le morceau est en tonalité de ''sol'' mineur naturel (donc avec un ''fa''♮) avec ajout de la note bleue (''{{lang|en|blue note}}'', quinte diminuée, ''ré''♭), et les accords composant le thème sont G<sup>5</sup>, B♭<sup>5</sup>, C<sup>5</sup> et D♭<sup>5</sup>, ce dernier accord étant l'accord sur la note bleue et pouvant être considéré comme une appoggiature (indiqué entre parenthèse ci-après). On a donc ''a priori'', sur les deux premières mesures, une progression {{Times New Roman|I-III-IV}} puis {{Times New Roman|I-III-(♭V)-IV}}. Durant la majeure partie du thème, la guitare basse tient la note ''sol'' en pédale. {{note|En jazz, la qualité « <sup>5</sup> » indique que l'on n'a que la quinte (et donc pas la tierce), contrairement à la notation de basse chiffrée.}} : {{lien web | url = https://www.dailymotion.com/video/x5ili04 | titre = Deep Purple — Smoke on the Water (Live at Montreux 2006) | auteur = Deep Purple | site = Dailymotion | date = 2016 | consulté le = 2020-12-31 }} Cependant, cette progression forme une mélodie, on peut donc plus la voir comme un contrepoint, la superposition de deux voies ayant un mouvement conjoint, joué par un seul instrument, la guitare, la voie 2 étant jouée une quarte juste en dessous de la voie 1 (la quarte juste descendante étant le renversement de la quinte juste ascendante) : * voie 1 (aigu) : | ''sol'' - ''si''♭ - ''do'' | ''sol'' - ''si''♭ - (''ré''♭) - ''do'' | ; * voie 2 (grave) : | ''ré'' - ''fa'' - ''sol'' | ''ré'' - ''fa'' - (''la''♭) - ''sol'' |. En se basant sur la basse (''sol'' en pédale), nous pouvons considérer que ces deux mesures sont accompagnées d'un accord de Gm<sup>7</sup> (''sol''-''si''♭-''ré''-''fa''), chaque accord de la mélodie comprenant à chaque fois au moins une note de cet accord à l'exception de l'appogiature. {| class="wikitable" |+ Mise en évidence des notes de l'accord Gm<sup>7</sup> |- ! scope="row" | Accords | G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> |- ! scope="row" | Voie 1 | '''''sol''''' || '''''si''♭''' || ''do'' |- ! scope="row" | Voie 2 | '''''ré''''' || '''''fa''''' || '''''sol''''' |- ! scope="row" | Basse | '''''sol''''' || '''''sol''''' || '''''sol''''' |} Sur les deux mesures suivantes, la basse varie et suit les accords de la guitare avec un retard sur le dernier accord : {| class="wikitable" |+ Voies sur les mesure 3-4 du thème |- ! scope="row" | Accords | G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> || B♭<sup>5</sup> || G<sup>5</sup> |- ! scope="row" | Voie 1 | ''sol'' || ''si''♭ || ''do'' || ''si''♭ || ''sol'' |- ! scope="row" | Voie 2 | ''ré'' || ''fa'' || ''sol'' || ''fa'' || ''ré'' |- ! scope="row" | Basse | ''sol'' || ''sol'' || ''do'' || ''si''♭ || ''si''♭-''sol'' |} Le couplet de cette chanson est aussi organisé sur une progression de quatre mesures, la guitare faisant des arpèges sur les accords G<sup>5</sup> (''sol''-''ré''-''sol'') et F<sup>5</sup> (''fa''-''do''-''fa'') : : | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-F<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | soit une progression {{Times New Roman|<nowiki>| I-I | I-I | I-VII | I-I |</nowiki>}}. Nous pouvons aussi harmoniser le riff du thème sur cette progression, avec un accord F (''fa''-''la''-''do'') ; nous pouvons aussi nous rappeler que l'accord sur le degré {{Times New Roman|VII}} est plus volontiers considéré comme un accord de septième de dominante {{Times New Roman|V<sup>7</sup>}}, soit ici un accord Dm<sup>7</sup> (''ré''-''fa''-''la''-''do''). On peut donc considérer la progression harmonique sur le thème : : | Gm-Gm | Gm-Gm | Gm-F ou Dm<sup>7</sup> | Gm-Gm |. Cette analyse permet de proposer une harmonisation enrichie du morceau, tout en se rappelant qu'une des forces du morceau initial est justement la simplicité de sa structure, qui fait ressortir la virtuosité des musiciens. Nous pouvons ainsi comparer la version album à la version concert avec orchestre ou à la version latino de Pat Boone. À l'inverse, le groupe Psychostrip, dans une version grunge, a remplacé les accords par une ligne mélodique : * le thème ne contient plus qu'une seule voie (la guitare ne joue pas des accords de puissance) ; * dans les mesures 9 et 10, la deuxième guitare joue en contrepoint de type mouvement inverse, qui est en fait la voie 2 jouée en miroir ; * l'arpège sur le couplet est remplacé par une ligne mélodique en ostinato sur une gamme blues. {| class="wikitable" |+ Contrepoint sur les mesures 9 et 10 |- ! scope="row" | Guitare 1 | ''sol'' ↗ ''si''♭ ↗ ''do'' |- ! scope="row" | Guitare 2 | ''sol'' ↘ ''fa'' ↘ ''ré'' |} * {{lien web | url = https://www.dailymotion.com/video/x5ik234 | titre = Deep Purple — Smoke on the Water (In Concert with the London Symphony Orchestra, 1999) | auteur = Deep Purple | site = Dailymotion | date = 2016 | consulté le = 2020-12-31 }} * {{lien web | url = https://www.youtube.com/watch?v=MtUuNzVROIg | titre = Pat Boone — Smoke on the Water (In a Metal Mood, No More Mr. Nice Guy, 1997) | auteur = Orrore a 33 Giri | site = YouTube | date = 2019-06-24 | consulté le = 2020-12-31 }} * {{lien web | url = https://www.youtube.com/watch?v=n7zLlZ8B0Bk | titre = Smoke on the Water (Heroes, 1993) | auteur = Psychostrip | site = YouTube | date = 2018-06-20 | consulté le = 2020-12-31 }} == Accords et improvisation == Nous avons vu précédemment (chapitre ''[[../Gammes et intervalles#Modes et improvisation|Gammes et intervalles &gt; Modes et improvisation]]'') que le choix d'un mode adapté permet d'improviser sur un accord. L'harmonisation des gammes permet, en inversant le processus, d'étendre notre palette : il suffit de repérer l'accord sur une harmonisaiton de gamme, et d'utiliser cette gamme-là, dans le mode correspondant du degré de l'accord (voir ci-dessus ''[[#Harmonisation par des accords de septième|Harmonisation par des accords de septième]]''). Par exemple, nous avons vu que l'accord sur le septième degré d'une gamme majeure était un accord demi-diminué ; nous savons donc que sur un accord demi-diminué, nous pouvons improviser sur le mode correspondant au septième degré, soit le mode de ''si'' (locrien). Un accord de septième de dominante étant commun aux deux tonalités homonymes (par exemple ''fa'' majeur et ''fa'' mineur pour un ''do''<sup>7</sup><sub>+</sub> / C<sup>7</sup>), nous pouvons utiliser le mode de ''sol'' de la gamme majeure (mixolydien) ou de la gamme mineure mineure (mode phrygien dominant, ou phrygien espagnol) pour improviser. Mais l'accord de septième de dominante est aussi l'accord au début d'une grille blues ; on peut donc improviser avec une gamme blues, même si la tierce est majeure dans l'accord et mineure dans la gamme. [[Fichier:Mode improvisation accords do complet.svg]] == Autres accords courants == [[fichier:Cluster cdefg.png|vignette|Agrégat ''do - ré - mi - fa - sol''.]] Nous avons vu précédemment l'harmonisation des tonalités majeures et mineures harmoniques par des triades et des accords de septième ; certains accords étant rarement utilisés (l'accord sur le degré {{Times New Roman|III}} et, pour les tonalités mineures harmoniques, l'accord sur la tonique), certains accords étant utilisés comme des accords sur un autre degré (les accords sur la sensible étant considérés comme des accords de dominante sans fondamentale). Dans l'absolu, on peut utiliser n'importe quelle combinaison de notes, jusqu'aux agrégats, ou ''{{lang|en|clusters}}'' (mot anglais signifiant « amas », « grappe ») : un ensemble de notes contigües, séparées par des intervalles de seconde. Dans la pratique, on reste souvent sur des accords composés de superpositions de tierces, sauf dans le cas de transitions (voir la section ''[[#Notes étrangères|Notes étrangère]]''). === En musique classique === On utilise parfois des accords dont les notes ne sont pas dans la tonalité (hors modulation). Il peut s'agir d'accords de passage, de notes étrangères, par exemple utilisant un chromatisme (mouvement conjoint par demi-tons). Outre les accords de passage, les autres accords que l'on rencontre couramment en musique classique sont les accords de neuvième, et les accords de onzième et treizième sur tonique. Ces accords sont simplement obtenus en continuant à empiler les tierces. Il n'y a pas d'accord d'ordre supérieur car la quinzième est deux octaves au-dessus de la fondamentale. Comme pour les accords de septième, on distingue les accords de neuvième de dominante et les accords de neuvième d'espèce. Dans le cas de la neuvième de dominante, il y a une différence entre les tonalités majeures et mineures : l'intervalle de neuvième est respectivement majeur et mineur. Les chiffrages des renversements peuvent donc différer. Comme pour les accords de septième de dominante, on considère que les accords de septième sur le degré {{Times New Roman|VI}} sont en fait des accords de neuvième de dominante sans fondamentale. Les accords de neuvième d'espèce sont en général préparés et résolus. Préparés : la neuvième étant une note dissonante (c'est à une octave près la seconde de la fondamentale), l'accord qui précède doit contenir cette note, mais dans un accord consonant ; la neuvième est donc commune avec l'accord précédent. Résolus : la dissonance est résolue en abaissant la neuvième par un mouvement conjoint. Par exemple, en tonalité de ''do'' majeur, si l'on veut utiliser un accord de neuvième d'espèce sur la tonique ''(do - mi - sol - si - ré)'', on peut utiliser avant un accord de dominante ''(sol - si - ré)'' en préparation puis un accord parfait sur le degré {{Times New Roman|IV}} ''(fa - la - do)'' en résolution ; nous avons donc sur la voie la plus aigüe la succession ''ré'' (consonant) - ''ré'' (dissonant) - ''do'' (consonant). On rencontre également parfois des accords de onzième et de treizième. On omet en général la tierce, car elle est dissonante avec la onzième. L'accord le plus fréquemment rencontré est l'accord sur la tonique : on considère alors que c'est un accord sur la dominante que l'on a enrichi « par le bas », en ajoutant une quinte inférieure. par exemple, dans la tonalité de ''do'' majeur, l'accord ''do - sol - si - ré - fa'' est considéré comme un accord de septième de dominante sur tonique, le degré étant noté « {{Times New Roman|V}}/{{Times New Roman|I}} ». De même pour l'accord ''do - sol - si - ré - fa - la'' qui est considéré comme un accord de neuvième de dominante sur tonique. === En jazz === En jazz, on utilise fréquemment l'accord de sixte à la place de l'accord de septième majeure sur la tonique. Par exemple, en ''do'' majeur, on utilise l'accord C<sup>6</sup> ''(do - mi - sol - la)'' à la place de C<sup>Δ</sup> ''(do - mi - sol - si)''. On peut noter que C<sup>6</sup> est un renversement de Am<sup>7</sup> et pourrait donc se noter Am<sup>7</sup>/C ; cependant, le fait de le noter C<sup>6</sup> indique que l'on a bien un accord sur la tonique qui s'inscrit dans la tonalité de ''do'' majeur (et non, par exemple, de ''la'' mineur naturelle) — par rapport à l'harmonie fonctionnelle, on remarquera que Am<sup>7</sup> a une fonction tonique, l'utilisation d'un renversement de Am<sup>7</sup> à la place d'un accord de C<sup>Δ</sup> est donc logique. Les accords de neuvième, onzième et treizième sont utilisés comme accords de septième enrichis. Le chiffrage suit les règles habituelles : on ajoute un « 9 », un « 11 » ou un « 13 » au chiffrage de l'accord de septième. On utilise également des accords dits « suspendus » : ce sont des accords de transition qui sont obtenus en prenant une triade majeure ou mineure et en remplaçant la tierce par la quarte juste (cas le plus fréquent) ou la seconde majeure. Plus particulièrement, lorsque l'on parle simplement « d'accord suspendu » sans plus de précision, cela désigne l'accord de neuvième avec une quarte suspendue, noté « 9sus4 » ou simplement « sus ». == L'harmonie tonale == L'harmonie tonale est un ensemble de règle assez strictes qui s'appliquent dans la musique savante européenne, de la période baroque à la période classique classique ({{pc|xiv}}<sup>e</sup>-{{pc|xviii}}<sup>e</sup> siècle). Certaines règles sont encore largement appliquées dans divers styles musicaux actuels, y compris populaire (rock, rap…), d'autres sont au contraire ignorées (par exemple, un enchaînement de plusieurs accords de même qualité forme un mouvement parallèle, ce qui est proscrit en harmonie tonale). De nos jours, on peut voir ces règles comme des règles « de bon goût », et leur application stricte comme une manière de composer « à la manière de ». Précédemment, nous avons vu la progression des accords. Ci-après, nous abordons aussi la manière dont les notes de l'accord sont réparties entre plusieurs voix, et comment on construit chaque voix. === Concepts fondamentaux === ; Consonance : Les intervalles sont considérés comme « plus ou moins consonants » : :* consonance parfaite : unisson, quinte et octave ; :* consonance mixte (parfaite dans certains contextes, imparfaite dans d'autres) : quarte ; :* consonance imparfaite : tierce et sixte ; :* dissonance : seconde et septième. ; Degrés : Certains degrés sont considérés comme « forts », « meilleurs », ce sont les « notes tonales » : {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (sous-dominante) et {{Times New Roman|V}} (dominante). [[Fichier:Mouvements harmoniques.svg|vignette|upright=0.75|Mouvements harmoniques.]] ; Mouvements : Le mouvement décrit la manière dont les voix évoluent les unes par rapport aux autres : :# Mouvement parallèle : les voix sont séparées par un intervalle constant. :# Mouvement oblique : une voix reste constante, c'est le bourdon ; l'autre monte ou descend. :# Mouvement contraire : une voix descend, l'autre monte. :# Échange de voix : les voix échangent de note ; les mélodies se croisent mais on a toujours le même intervalle harmonique. {{clear}} === Premières règles === ; Règle du plus court chemin : Quand on passe d'un accord à l'autre, la répartition des notes se fait de sorte que chaque voix fait le plus petit mouvement possible. Notamment : si les deux accords ont des notes en commun, alors les voix concernées gardent la même note. : Les deux voix les plus importantes sont la voix aigüe — soprano — et la voix la plus grave — basse. Ces deux voix sont relativement libres : la voix de soprano a la mélodie, la voix de basse fonde l'harmonie. La règle du plus court chemin s'applique surtout aux voix intermédiaires ; si l'on a des mouvements conjoints, ou du moins de petits intervalles — c'est le sens de la règle du plus court chemin —, alors les voix sont plus faciles à interpréter. Cette règle évite également que les voix n'empiètent l'une sur l'autre (voir la règle « éviter le croisement des voix »). ; Éviter les consonances parfaites consécutives :* Lorsque deux voix sont à l'unisson ou à l'octave, elles ne doivent pas garder le même intervalle, l'effet serait trop plat. :* Lorsque deux voix sont à la quarte ou à la quinte, elles ne doivent pas garder le même intervalle, car l'effet est trop dur. : Pour éviter cela, lorsque l'on part d'un intervalle juste, on a intérêt à pratiquer un mouvement contraire aux voix qui ne gardent pas la même note, ou au moins un mouvement direct : les voix vont dans le même sens, mais l'intervalle change. : Notez que même avec le mouvement contraire, on peut avoir des consonances parfaites consécutives, par exemple si une voix fait ''do'' aigu ↗ ''sol'' aigu et l'autre ''sol'' médium ↘ ''do'' grave. : L'interdiction des consonances parfaites consécutives n'a pas été toujours appliquée, le mouvement parallèle strict a d'ailleurs été le premier procédé utilisé dans la musique religieuse au {{pc|x}}<sup>e</sup> siècle. On peut par exemple utiliser des quintes parallèles pour donner un style médiéval au morceau. On peut également utiliser des octaves parallèles sur plusieurs notes afin de créer un effet de renforcement de la mélodie. : Par ailleurs, les consonances parfaites consécutives sont acceptées lorsqu'il s'agit d'une cadence (transition entre deux parties ou bien conclusion du morceau). ; Éviter le croisement des voix : Les voix sont organisées de la plus grave à la plus aigüe. Deux voix n'étant pas à l'unisson, celle qui est plus aigüe ne doit pas devenir la plus grave et ''vice versa''. ; Soigner la partie soprano : Comme c'est celle qu'on entend le mieux, c'est en général celle qui porte la mélodie principale. On lui applique des règles spécifiques : :# Si elle chante la sensible dans un accord de dominante ({{Times New Roman|V}}), alors elle doit monter à la tonique, c'est-à-dire que la note suivante sera la tonique située un demi-ton au dessus. :# Si l'on arrive à une quinte ou une octave entre les parties basse et soprano par un mouvement direct, alors sur la partie soprano, le mouvement doit être conjoint. On doit donc arriver à cette situation par des notes voisines au soprano. ; Préférer certains accords : Les deux degrés les plus importants sont la tonique ({{Times New Roman|I}}) et la dominante ({{Times New Roman|V}}), les accords correspondants ont donc une importance particulière. : À l'inverse, l'accord de sensible ({{Times New Roman|VII}}) n'est pas considéré comme ayant une fonction harmonique forte. On le considère comme un accord de dominante affaibli. En tonalité mineure, on évite également l'accord de médiante ({{Times New Roman|III}}). : Donc on utilise en priorité les accords de : :# {{Times New Roman|I}} et {{Times New Roman|V}}. :# Puis {{Times New Roman|II}}, {{Times New Roman|IV}}, {{Times New Roman|VI}} ; et {{Times New Roman|III}} en mode majeur. :# On évite {{Times New Roman|VII}} ; et {{Times New Roman|III}} en mode mineur. ; Préférer certains enchaînements : Les enchaînements d'accord peuvent être classés par ordre de préférence. Par ordre de préférence décroissante (du « meilleur » au « moins bon ») : :# Meilleurs enchaînements : quarte ascendante ou descendante. Notons que la quarte est le renversement de la quinte, on a donc des enchaînements stables et naturels, mais avec un intervalle plus court qu'un enchaînement de quintes. :# Bons enchaînements : tierce ascendante ou descendante. Les accords consécutifs ont deux notes en commun. :# Enchaînements médiocres : seconde ascendante ou descendante. Les accords sont voisins, mais ils n'ont aucune note en commun. On les utilise de préférence en mouvement ascendant, et on utilise surtout les enchaînements {{Times New Roman|IV}}-{{Times New Roman|V}}, {{Times New Roman|V}}-{{Times New Roman|VI}} et éventuellement {{Times New Roman|I}}-{{Times New Roman|II}}. :# Les autres enchaînements sont à éviter. : On peut atténuer l'effet d'un enchaînement médiocre en plaçant le second accord sur un temps faible ou bien en passant par un accord intermédiaire. [[Fichier:Progression Vplus4 I6.svg|thumb|Résolution d'un accord de triton (quarte sensible) vers l'accord de sixte de la tonique.]] ; La septième descend par mouvement conjoint : Dans un accord de septième de dominante, la septième — qui est donc le degré {{Times New Roman|IV}} — descend par mouvement conjoint — elle est donc suivie du degré {{Times New Roman|III}}. : Corolaire : un accord {{Times New Roman|V}}<sup>+4</sup> se résout par un accord {{Times New Roman|I}}<sup>6</sup> : on a bien un enchaînement {{Times New Roman|V}} → {{Times New Roman|I}}, et la 7{{e}} (degré {{Times New Roman|IV}}), qui est la basse de l'accord {{Times New Roman|V}}<sup>+4</sup>, descend d'un degré pour donner la basse de l'accord {{Times New Roman|I}}<sup>6</sup> (degré {{Times New Roman|III}}). {{clear}} [[Fichier:Progression I64 V7plus I5.svg|thumb|Accord de sixte et de quarte cadentiel.]] ; Un accord de sixte et quarte est un accord de passage : Le second renversement d'un accord parfait est soit une appoggiature, soit un accord de passage, soit un accord de broderie. : S'il s'agit de l'accord de tonique {{Times New Roman|I}}<sup>6</sup><sub>4</sub>, c'est « accord de sixte et quarte de cadence », l'appoggiature de l'accord de dominante de la cadence parfaite. {{clear}} Mais il faut appliquer ces règles avec discernement. Par exemple, la voix la plus aigüe est celle qui s'entend le mieux, c'est donc elle qui porte la mélodie principale. Il est important qu'elle reste la plus aigüe. La voix la plus grave porte l'harmonie, elle pose les accords, il est donc également important qu'elle reste la plus grave. Ceci a deux conséquences : # Ces deux voix extrêmes peuvent avoir des intervalles mélodiques importants et donc déroger à la règle du plus court chemin : la voix aigüe parce que la mélodie prime, la voix de basse parce que la progression d'accords prime. # Les croisements des voix intermédiaires sont moins critiques. Par ailleurs, si l'on applique strictement toutes les règles « meilleurs accords, meilleurs enchaînements », on produit un effet conventionnel, stéréotypé. Il est donc important d'utiliser les solutions « moins bonnes », « médiocres » pour apporter de la variété. Ajoutons que les renversements d'accords permettent d'avoir plus de souplesse : on reste sur le même accord, mais on enrichit la mélodie sur chaque voix. Le ''Bolero'' de Maurice Ravel (1928) brise un certain nombre de ces règles. Par exemple, de la mesure 39 à la mesure 59, la harpe joue des secondes. De la mesure 149 à la mesure 165, les piccolo jouent à la sixte, dans des mouvement strictement parallèle, ce qui donne d'ailleurs une sonorité étrange. À partir de la mesure 239, de nombreux instruments jouent en mouvement parallèles (piccolos, flûtes, hautbois, cor, clarinettes et violons). === Application === [[Fichier:Harmonisation possible de frere jacques exercice.svg|vignette|Exercice : harmoniser ''Frère Jacques''.]] Harmoniser ''Frère Jacques''. Nous considérons un morceau à quatre voix : basse, ténor, alto et soprano. La soprano chante la mélodie de ''Frère Jacques''. L'exercice consiste à proposer l'écriture des trois autres voix en respectant les règles énoncées ci-dessus. Pour simplifier, nous ajoutons les contraintes suivantes : * toutes les voix chantent des blanches ; * nous nous limitons aux accords de quinte (accords de trois sons composés d'une tierce et d'une quinte) sans avoir recours à leurs renversements (accords de sixte, accords de sixte et de quarte). Les notes à gauche de la portée indiquent la tessiture (ou ambitus), l'amplitude que peut chanter la voix. {{clear}} {{boîte déroulante/début|titre=Solution possible}} [[Fichier:Harmonisation possible de frere jacques solution.svg|vignette|Harmonisation possible de ''Frère Jacques'' (solution de l'exercice).]] Il n'y a pas qu'une solution possible. Le premier accord doit contenir un ''do''. Nous sommes manifestement en tonalité de ''do'' majeur, nous proposons de commencer par l'accord parfait de ''do'' majeur, I<sup>5</sup>. Le deuxième accord doit comporter un ''ré''. Si nous utilisons l'accord de quinte de ''ré'', nous allons créer une quinte parallèle. Nous pourrions utiliser un renversement, mais nous nous imposons de chercher un autre accord. Il peut s'agir de l'accord ''si''<sup>5</sup> ''(si-ré-fa)'' ou de l'accord de ''sol''<sup>5</sup> ''(sol-si-ré)''. La dernière solution permet d'utiliser l'accord de dominante qui est un accord important de la tonalité. La règle du plus court chemin imposerait le ''sol'' grave pour la partie de basse, mais cela est proche de la limite du chanteur, nous préférons passer au ''sol'' aigu, plus facile à chanter. Nous vérifions qu'il n'y a pas de quinte parallèle : l'intervalle ascendant ''do-sol'' (basse-alto) devient ''sol-si'' (3<sup>ce</sup>), l'intervalle descendant ''do-sol'' (soprano-alto) devient ''ré-si'' (3<sup>ce</sup>). De la même manière, pour le troisième accord, nous ne pouvons pas passer à un accord de ''la''<sup>5</sup> pour éviter une quinte parallèle. Nous avons le choix entre ''do''<sup>5</sup> ''(do-mi-sol)'' et ''mi''<sup>5</sup> ''(mi-sol-si)''. Nous préférons revenir à l'accord de fondamental, solution très stable (l'enchaînement {{Times New Roman|V}}-{{Times New Roman|I}} formant une cadence parfaite). Pour le quatrième accord, nous pourrions rester sur l'accord parfait de ''do'' mais cela planterait en quelque sorte la fin du morceau puisque l'on resterait sur la cadence parfaite ; or, nous connaissons le morceau et savons qu'il n'est pas fini. Nous choisissons l'accord de ''la''<sup>5</sup> qui est une sixte ascendante ({{Times New Roman|I}}-{{Times New Roman|VI}}). Nos aurions pu répartir les voix différemment. Par exemple : * alto : ''sol''-''si''-''sol''-''do'' ; * ténor : ''mi''-''ré''-''mi''-''mi''. {{boîte déroulante/fin}} [[Fichier:Harmonisation possible de frere jacques.midi|vignette|Fichier son correspondant.]] {{clear}} == Annexe == === Accords en musique classique === Un accord est un ensemble de notes jouées simultanément. Il peut s'agir : * de notes jouées par plusieurs instruments ; * de notes jouées par un même instrument : piano, clavecin, orgue, guitare, harpe (la plupart des instruments à clavier et des instruments à corde). Pour deux notes jouées simultanément, on parle d'intervalle « harmonique » (par opposition à l'intervalle « mélodique » qui concerne les notes jouées successivement). Les notes répétées à différentes octaves ne changent pas la nature de l'accord. La musique classique considère en général des empilements de tierces ; un accord de trois notes sera constitué de deux tierces successives, un accord de quatre notes de trois tierces… Lorsque tous les intervalles sont des intervalles impairs — tierces, quintes, septièmes, neuvièmes, onzièmes, treizièmes… — alors l'accord est dit « à l'état fondamental » (ou encore « primitif » ou « direct »). La note de la plus grave est appelée « fondamentale » de l'accord. Lorsque l'accord comporte un ou des intervalles pairs, l'accord est dit « renversé » ; la note la plus grave est appelée « basse ». De manière plus générale, l'accord est dit à l'état fondamental lorsque la basse est aussi la fondamentale. On a donc un état idéal de l'accord (état canonique) — un empilement strict de tierces — et l'état réel de l'accord — l'empilement des notes réellement jouées, avec d'éventuels redoublements, omissions et inversions ; et seule la basse indique si l'accord est à l'état fondamental ou renversé. Le chiffrage dit de « basse continue » ''({{lang|it|basso continuo}})'' désigne la représentation d'un accord sous la forme d'un ou plusieurs chiffres arabes et éventuellement d'un chiffre romain. ==== Accords de trois notes ==== En musique classique, les seuls accords considérés comme parfaitement consonants, c'est-à-dire sonnant agréablement à l'oreille, sont appelés « accords parfaits ». Si l'on prend une tonalité et un mode donné, alors l'accord construit par superposition es degrés I, III et V de cette gamme porte le nom de la gamme qui l'a généré. [[fichier:Accord do majeur chiffre.svg|vignette|upright=0.5|Accord parfait de ''do'' majeur chiffré.]] Par exemple : * « l'accord parfait de ''do'' majeur » est composé des notes ''do'', ''mi'' et ''sol'' ; * « l'accord parfait de ''la'' mineur » est composé des notes ''la'', ''do'' et ''mi''. Un accord parfait majeur est donc composé, en partant de la fondamentale, d'une tierce majeure et d'une quinte juste. Un accord parfait mineur est composé d'une tierce mineure et d'une quinte juste. L'accord parfait à l'état fondamental est appelé « accord de quinte » et est simplement chiffré « 5 » pour indiquer la quinte. On peut également commencer un accord sur sa deuxième ou sa troisième note, en faisant monter celle(s) qui précède(nt) à l'octave suivante. On parle alors de « renversement d'accord » ou d'accord « renversé ». [[Fichier:Accord do majeur renversements chiffre.svg|vignette|upright=0.75|Accord parfait de ''do'' majeur et ses renversements, chiffrés.]] Par exemple, * le premier renversement de l'accord parfait de ''do'' majeur est :<br /> ''mi'', ''sol'', ''do'' ; * le second renversement de l'accord parfait de do majeur est :<br /> ''sol'', ''do'', ''mi''. Les notes conservent leur nom de « fondamentale », « tierce » et « quinte » malgré le changement d'ordre. La note la plus grave est appelée « basse ». Dans le cas du premier renversement, le deuxième note est la tierce de la basse (la note la plus grave) et la troisième note est la sixte ; le chiffrage en chiffres arabes est donc « 6 » (puisque l'on omet la tierce) et l'accord est appelé « accord de sixte ». Pour le deuxième renversement, les intervalles sont la quarte et la sixte, le chiffrage est donc « 6-4 » et l'accord est appelé « accord de sixte et de quarte ». Dans tous les cas, on chiffre le degré on considérant la fondamentale, par exemple {{Times New Roman|I}} si l'accord est construit sur la tonique de la gamme. Les autres accords de trois notes que l'on rencontre sont : * l'accord de quinte diminuée, constitué d'une tierce mineure et d'une quinte diminuée ; lorsqu'il est construit sur le septième degré d'une gamme, on considère que c'est un accord de septième de dominante sans fondamentale (voir plus bas), le degré est donc indiqué « “{{Times New Roman|V}}” » (cinq entre guillemets) et non « {{Times New Roman|VII}} » ; * l'accord de quinte augmenté : il est composé d'une tierce majeure et qu'une quinte augmentée. Dans le tableau ci-dessous, * « m » désigne un intervalle mineur ; * « M » un intervalle majeur ou le mode majeur ; * « J » un intervalle juste ; * « d » un intervalle diminué ; * « A » un intervalle augmenté ; * « mh » le mode mineur harmonique ; * « ma » le mode mineur ascendant ; * « md » le mode mineur descendant. {| class="wikitable" |+ Accords de trois notes ! scope="col" rowspan="2" | Nom ! scope="col" rowspan="2" | 3<sup>ce</sup> ! scope="col" rowspan="2" | 5<sup>te</sup> ! scope="col" rowspan="2" | État fondamental ! scope="col" rowspan="2" | 1<sup>er</sup> renversement ! scope="col" rowspan="2" | 2<sup>nd</sup> renversement ! scope="col" colspan="4"| Construit sur les degrés |- ! scope="col" | M ! scope="col" | mh ! scope="col" | ma ! scope="col" | md |- | Accord parfait<br /> majeur || M || J | accord de quinte || accord de sixte || accord de<br />sixte et de quarte | {{Times New Roman|I, IV, V}} || {{Times New Roman|V, VI}} || {{Times New Roman|IV, V}} || {{Times New Roman|III, VI, VII}} |- | Accord parfait<br /> mineur || m || J | accord de quinte || accord de sixte || accord de<br />sixte et de quarte | {{Times New Roman|II, III, VI}} || {{Times New Roman|I, IV}} || {{Times New Roman|I, II}} || {{Times New Roman|I, IV, V}} |- | Accord de<br />quinte diminuée || m || d | accord de<br />quinte diminuée || accord de<br />sixte sensible<br />sans fondamentale || accord de triton<br />sans fondamentale | {{Times New Roman|VII (“V”)}} || {{Times New Roman|II, VII (“V”)}} || {{Times New Roman|VI, VII (“V”)}} || {{Times New Roman|II}} |- | Accord de<br />quinte augmentée || M || A | accord de<br />quinte augmentée || accord de sixte<br />et de tierce sensible || accord de sixte et de quarte<br />sur sensible | &nbsp; || {{Times New Roman|III}} || {{Times New Roman|III}} || &nbsp; |} ==== Accords de quatre notes ==== Les accords de quatre notes sont des accord composés de trois tierces superposées. La dernière note étant le septième degré de la gamme, on parle aussi d'accords de septième. Ces accords sont dissonants : ils contiennent un intervalle de septième (soit une octave montante suivie d'une seconde descendante). Ils laissent donc une impression de « tension ». Il existe sept différents types d'accords, ou « espèces ». Citons l'accord de septième de dominante, l'accord de septième mineure et l'accord de septième majeure. ===== L'accord de septième de dominante ===== [[Fichier:Accord 7e dominante do majeur renversements chiffre.svg|vignette|Accord de septième de dominante de ''do'' majeur et ses renversements, chiffrés.]] L'accord de septième de dominante est l'empilement de trois tierces à partir de la dominante de la gamme, c'est-à-dire du {{Times New Roman|V}}<sup>e</sup> degré. Par exemple, l'accord de septième de dominante de ''do'' majeur est l'accord ''sol''-''si''-''ré''-''fa'', et l'accord de septième de dominante de ''la'' mineur est ''mi''-''sol''♯-''si''-''ré''. L'accord de septième de dominante dont la fondamentale est ''do'' (''do''-''mi''-''sol''-''si''♭) appartient à la gamme de ''fa'' majeur. Que le mode soit majeur ou mineur, il est composé d'une tierce majeure, d'une quinte juste et d'une septième mineure (c'est un accord parfait majeur auquel on ajoute une septième mineure). C'est de loin l'accord de septième le plus utilisé ; il apparaît au {{pc|xvii}}<sup>e</sup> en musique classique. Dans son état fondamental, son chiffrage est {{Times New Roman|V 7/+}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}). Le signe plus indique la sensible. Son premier renversement est appelé « accord de quinte diminuée et sixte » et est noté {{Times New Roman|V 6/<s>5</s>}} (ou {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}). Son deuxième renversement est appelé « accord de sixte sensible », puisque la sixte de l'accord est la sensible de la gamme, et est noté {{Times New Roman|V +6}} (ou {{Times New Roman|V<sup>+6</sup>}}). Son troisième renversement est appelé « accord de quarte sensible » et est noté {{Times New Roman|V +4}} (ou {{Times New Roman|V<sup>+4</sup>}}). [[Fichier:Accord 7e dominante sans fondamentale do majeur renversements chiffre.svg|vignette|Accord de septième de dominante sans fondamentale de ''do'' majeur et ses renversements, chiffrés.]] On utilise aussi l'accord de septième de dominante sans fondamentale ; c'est alors un accord de trois notes. Dans son état fondamental, c'est un « accord de quinte diminuée » placé sur le {{Times New Roman|VII}}<sup>e</sup> degré (mais c'est bien un accord construit sur le {{Times New Roman|V}}<sup>e</sup> degré), noté {{Times New Roman|“V” <s>5</s>}} (ou {{Times New Roman|“V”<sup><s>5</s></sup>}}). Notez les guillemets qui indiquent que la fondamentale V est absente. Dans son premier renversement, c'est un « accord de sixte sensible sans fondamentale » noté {{Times New Roman|“V” +6/3}} (ou {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}}). Dans son second renversement, c'est un « accord de triton sans fondamentale » (puisque le premier intervalle est une quarte augmentée qui comporte trois tons) noté {{Times New Roman|“V” 6/+4}} (ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}). Notons qu'un accord de septième de dominante n'a pas toujours la dominante pour fondamentale : tout accord composé d'une tierce majeure, d'une quinte juste et d'une septième mineure est un accord de septième de dominante et est chiffré {{Times New Roman|<sup>7</sup><sub>+</sub>}}, quel que soit le degré sur lequel il est bâti (certaines notes peuvent avoir une altération accidentelle). ===== Les accords de septième d'espèce ===== Les autres accords de septièmes sont dits « d'espèce ». L'accord de septième mineure est l'accord de septième formé sur la fondamentale d'une gamme mineure ''naturelle''. Par exemple, l'accord de septième mineure de ''la'' est ''la''-''do''-''mi''-''sol''. Il est composé d'une tierce mineure, d'une quinte juste et d'une septième mineure (c'est un accord parfait mineur auquel on ajoute une septième mineure). L'accord de septième majeure est l'accord de septième formé sur la fondamentale d'une gamme majeure. Par exemple, L'accord de septième majeure de ''do'' est ''do''-''mi''-''sol''-''si''. Il est composé d'une tierce majeure, d'une quinte juste et d'une septième majeure (c'est un accord parfait majeur auquel on ajoute une septième majeure). ==== Utilisation du chiffrage ==== Le chiffrage est utilisé de deux manières. La première manière, c'est la notation de la basse continue. La basse continue est une technique d'improvisation utilisée dans le baroque pour l'accompagnement d'instruments solistes. Sur la partition, on indique en général la note de basse de l'accord et le chiffrage en chiffres arabes. La seconde manière, c'est pour l'analyse d'une partition. Le fait de chiffrer les accords permet de mieux en comprendre la structure. De manière générale, on peut retenir que : * le chiffrage « 5 » indique un accord parfait, superposition d'une tierce (majeure ou mineure) et d'une quinte juste ; * le chiffrage « 6 » indique le premier renversement d'un accord parfait ; * le chiffrage « 6/4 » indique le second renversement d'un accord parfait ; * chiffrage « 7/+ » indique un accord de septième de dominante ; * le signe « + » indique en général que la note de l'intervalle est la sensible ; * un intervalle barré désigne un intervalle diminué. [[fichier:Accords gamme do majeur la mineur.svg|class=transparent| center | Principaux accords construits sur les gammes de ''do'' majeur et de ''la'' mineur harmonique.]] === Notation « jazz » === En jazz et de manière générale en musique rock et populaire, la base d'un accord est la triade composée d'une tierce (majeure ou mineure) et d'une quinte juste. Pour désigner un accord, on utilise la note fondamentale, éventuellement désigné par une lettre dans le système anglo-saxon (A pour ''la'' etc.), suivi d'une qualité (comme « m », « + »…). Les renversements ne sont pas notés de manière particulière, ils sont notés comme les formes fondamentales. Dans les deux tableaux suivants, la fondamentale est notée X (remplace le C pour un accord de ''do'', le D pour un accord de ''ré''…). La construction des accords est décrite par la suite. [[Fichier:Arbre accords triades 5d5J5A.svg|vignette|upright=1.5|Formation des triades présentée sous forme d'arbre.]] {| class="wikitable" |+ Notation des principales triades |- | ! scope="col" | Tierce<br />mineure (3m) ! scope="col" | Tierce<br />majeure (3M) |- ! scope="row" | Quinte diminuée (5d) | X<sup>o</sup>, Xm<sup>♭5</sup>, X–<sup>♭5</sup> || |- ! scope="row" | Quinte juste (5J) | Xm, X– || X |- ! scope="row" | Quinte augmentée (5A) | || X+, X<sup>♯5</sup> |} [[Fichier:Triades do.svg|class=transparent|center|Triades de do.]] {| class="wikitable" |+ Notation des principaux accords de septième |- | colspan="2" | ! scope="col" | Tierce<br />mineure (3m) ! scope="col" | Tierce<br />majeure (3M) |- ! scope="row" rowspan="2" | Quinte<br />diminuée (5d) ! scope="row" | Septième diminuée (7d) | X<sup>o7</sup> || |- ! scope="row" | Septième mineure (7m) | Xm<sup>7(♭5)</sup>, X–<sup>7(♭5)</sup>, X<sup>Ø</sup> || |- ! scope="row" rowspan="3" | Quinte<br />juste (5J) ! scope="row" | Sixte majeure (6M) | Xm<sup>6</sup> || X<sup>6</sup> |- ! scope="row" | Septième mineure (7m) | Xm<sup>7</sup>, X–<sup>7</sup> || X<sup>7</sup> |- ! scope="row" | Septième majeure (7M) | Xm<sup>maj7</sup>, X–<sup>maj7</sup>, Xm<sup>Δ</sup>, X–<sup>Δ</sup> || X<sup>maj7</sup>, X<sup>Δ</sup> |- ! scope="row" rowspan="2" | Quinte<br />augmentée (5A) ! scope="row" | Septième mineure (7m) | || X+<sup>7</sup> |- ! scope="row" | Septième majeure (7M) | || X+<sup>maj7</sup> |} [[Fichier:Arbre accords septieme.svg|class=transparent|center|Formation des accords de septième présentée sous forme d'arbre.]] &nbsp; &nbsp; [[Fichier:Accords do septieme.svg|class=transparent|center|Accord de do septième.]] On notera que l'intervalle de sixte majeure est l'enharmonique de celui de septième diminuée (6M = 7d). [[File:Principaux accords do.svg|class=transparent|center|Principaux accords de do.]] ==== Triades ==== ; Accords fondés sur une tierce majeure * accord parfait majeur : pas de notation *: p. ex. « ''do'' » ou « C » pour l'accord parfait de ''do'' majeur (''do'' - ''mi'' - ''sol'') ; Accords fondés sur une tierce mineure * accord parfait mineur : « m », « min » ou « – » *: « ''do'' m », « ''do'' – », « Cm », « C– »… pour l'accord parfait de ''do'' mineur (''do'' - ''mi''♭ - ''sol'') ==== Triades modifiées ==== ; Accords fondés sur une tierce majeure * accord augmenté (la quinte est augmentée) : aug, +, ♯5 *: « ''do'' aug », « ''do'' + », « ''do''<sup>♯5</sup> » « Caug », « C+ » ou « C<sup>♯5</sup> » pour l'accord de ''do'' augmenté (''do'' - ''mi'' - ''sol''♯) : L'accord augmenté est un empilement de tierces majeures. Ainsi, un accord augmenté a deux notes communes avec deux autres accords augmentés : C+ (''do'' - ''mi'' - ''sol''♯) a deux notes communes avec A♭+ (''la''♭ - ''do'' - ''mi'') et avec E+ (''mi'' - ''sol''♯ - ''si''♯) ; et on remarque que ces trois accords sont en fait enharmoniques (avec les enharmonies ''la''♭ = ''sol''♯ et ''si''♯ = ''do''). En effet, l'octave comporte six tons (sous la forme de cinq tons et deux demi-tons), et une tierce majeure comporte deux tons, on arrive donc à l'octave en ajoutant une tierce majeure à la dernière note de l'accord. ; Accords fondés sur une tierce mineure * accord diminué (la quinte est diminuée) : dim, o, ♭5 *: « ''do'' dim », « ''do''<sup>o</sup> », « ''do''<sup>♭5</sup> », « Cdim », « C<sup>o</sup> » ou « C<sup>♭5</sup> » pour l'accord de ''do'' diminuné (''do'' - ''mi''♭ - ''sol''♭) : On remarque que la quinte diminuée est l'enharmonique de la quarte augmentée et est l'intervalle appelé « triton » (car composé de trois tons). ; Accords fondés sur une tierce majeure ou mineure * accord suspendu de seconde : la tierce est remplacée par une seconde majeure : sus2 *: « ''do''<sup>sus2</sup> » ou « C<sup>sus2</sup> » pour l'accord de ''do'' majeur suspendu de seconde (''do''-''ré''-''sol'') * accord suspendu de quarte : la tierce est remplacée par une quarte juste : sus4 *: « ''do''<sup>sus4</sup> » ou « C<sup>sus4</sup> » pour l'accord de ''do'' majeur suspendu de quarte (''do''-''fa''-''sol'') ==== Triades appauvries ==== ; Accords fondés sur une tierce majeure ou mineure * accord de puissance : la tierce est omise, l'accord n'est constitué que de la fondamentale et de la quinte juste : 5 *: « ''do''<sup>5</sup> », « C<sup>5</sup> » pour l'accord de puissance de ''do'' (''do'' - ''la'') {{note|Très utilisé dans les musiques rock, hard rock et heavy metal, il est souvent joué renversé (''la'' - ''do'') ou bien avec l'ajout de l'octave (''do'' - ''la'' - ''do'').}} ==== Triades enrichies ==== ; Accords fondés sur une tierce majeure * accord de septième (la 7<sup>e</sup> est mineure) : 7 *: « ''do''<sup>7</sup> », « C<sup>7</sup> » pour l'accord de ''do'' septième, appelé « accord de septième de dominante de ''fa'' majeur » en musique classique (''do'' - ''mi'' - ''sol'' - ''si''♭) * accord de septième majeure : Δ, 7M ou maj7 *: « ''do'' <sup>Δ</sup> », « ''do'' <sup>maj7</sup> », « C<sup>Δ</sup> », « C<sup>7M</sup> »… pour l'accord de ''do'' septième majeure (''do'' - ''mi'' - ''sol'' - ''si'') ; Accords fondés sur une tierce mineure * accord de mineur septième (la tierce et la 7<sup>e</sup> sont mineures) : m7, min7 ou –7 *: « ''do'' m<sup>7</sup> », « ''do'' –<sup>7</sup> », « Cm<sup>7</sup> », « C–<sup>7</sup> »… pour l'accord de ''do'' mineur septième, appelé « accord de septième de dominante de ''fa'' mineur » en musique classique (''do'' - ''mi''♭ - ''sol'' - ''si''♭) * accord mineure septième majeure : m7M, m7maj, mΔ, –7M, –7maj, –Δ *: « ''do'' m<sup>7M</sup> », « ''do'' m<sup>maj7</sup> », « ''do'' –<sup>Δ</sup> », « Cm<sup>7M</sup> », « Cm<sup>maj7</sup> », « C–<sup>Δ</sup> »… pour l'accord de ''do'' mineur septième majeure (''do'' - ''mi''♭ - ''sol'' - ''si'') * accord de septième diminué (la quinte et la septième sont diminuée) : dim 7 ou o7 *: « ''do'' dim<sup>7</sup> », « ''do''<sup>o7</sup> », « Cdim<sup>7</sup> » ou « C<sup>o7</sup> » pour l'accord de ''do'' septième diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si''♭) * accord demi-diminué (seule la quinte est diminuée, la septième est mineure) : Ø ou –7(♭5) *: « ''do''<sup>Ø</sup> », « ''do''<sup>7(♭5)</sup> », « C<sup>Ø</sup> » ou « C<sup>7♭5</sup> » pour l'accord de ''do'' demi-diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si'') === Construction pythagoricienne des accords === Nous avons vu au débuts que lorsque l'on joue deux notes en même temps, leurs vibrations se superposent. Certaines superpositions créent un phénomène de battement désagréable, c'est le cas des secondes. Dans le cas d'une tierce majeure, les fréquences des notes quadruple et quintuple d'une même base : les fréquences s'écrivent 4׃<sub>0</sub> et 5׃<sub>0</sub>. Cette superposition de vibrations est agréable à l'oreille. Nous avons également vu que dans le cas d'une quinte juste, les fréquences sont le double et le triple d'une même base, ou encore le quadruple et sextuple si l'on considère la moitié de cette base. Ainsi, dans un accord parfait majeur, les fréquences des fondamentales des notes sont dans un rapport 4, 5, 6. De même, dans le cas d'un accord parfait mineur, les proportions sont de 1/6, 1/5 et 1/4. {{voir|[[../Caractéristiques_et_notation_des_sons_musicaux#Construction_pythagoricienne_et_gamme_de_sept_tons|Caractéristiques et notation des sons musicaux &gt; Construction pythagoricienne et gamme de sept tons]]}} === Un peu de physique : interférences === Les sons sont des vibrations. Lorsque l'on émet deux sons ou plus simultanément, les vibrations se superposent, on parle en physique « d'interférences ». Le modèle le plus simple pour décrire une vibration est la [[w:fr:Fonction sinus|fonction sinus]] : la pression de l'air P varie en fonction du temps ''t'' (en secondes, s), et l'on a pour un son « pur » : : P(''t'') ≈ sin(2π⋅ƒ⋅''t'') où ƒ est la fréquence (en hertz, Hz) du son. Si l'on émet deux sons de fréquence respective ƒ<sub>1</sub> et ƒ<sub>2</sub>, alors la pression vaut : : P(''t'') ≈ sin(2π⋅ƒ<sub>1</sub>⋅''t'') + sin(2π⋅ƒ<sub>2</sub>⋅''t''). Nous avons ici une [[w:fr:Identité trigonométrique#Transformation_de_sommes_en_produits,_ou_antilinéarisation|identité trigonométrique]] dite « antilinéarisation » : : <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{f_1 + f_2}{2}t \right ) \cdot \sin \left ( 2\pi \frac{f_1 - f_2}{2}t \right ).</math> On peut étudier simplement deux situations simples. [[Fichier:Battements interferentiels.png|vignette|Deux sons de fréquences proches créent des battements : la superposition d'une fréquence et d'une enveloppe.]] La première, c'est quand les fréquences ƒ<sub>1</sub> et ƒ<sub>2</sub> sont très proches. Alors, la moyenne (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2 est très proche de ƒ<sub>1</sub> et ƒ<sub>2</sub> ; et la demie différence (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2 est très proche de zéro. On a donc une enveloppe de fréquence très faible, (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2, dans laquelle s'inscrit un son de fréquence moyenne, (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2. C'est cette enveloppe de fréquence très faible qui crée les battements, désagréables à l'oreille. Sur l'image ci-contre, le premier trait rouge montre un instant où les vibrations sont opposées ; elles s'annulent, le son s'éteint. Le second trait rouge montre un instant où les vibrations sont en phase : elle s'ajoutent, le son est au plus fort. {{clear}} La seconde, c'est lorsque les deux fréquences sont des multiples entiers d'une même fréquence fondamentale ƒ<sub>0</sub> : ƒ<sub>1</sub> = ''n''<sub>1</sub>⋅ƒ<sub>0</sub> et ƒ<sub>0</sub> = ''n''<sub>0</sub>⋅ƒ<sub>0</sub>. On a alors : : <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{n_1 + n_2}{2}f_0 \cdot t \right ) \cdot \sin \left ( 2\pi \frac{n_1 - n_2}{2}f_0 \cdot t \right ).</math> On multiplie donc deux fonctions qui ont des fréquences multiples de ƒ<sub>0</sub>. La différence minimale entre ''n''<sub>1</sub> et ''n''<sub>2</sub> vaut 1 ; on a donc une enveloppe dont la fréquence est au minimum la moitié de ƒ<sub>0</sub>, c'est-à-dire un son une octave en dessous de ƒ<sub>0</sub>. Donc, cette enveloppe ne crée pas d'effet de battement, ou plutôt, le battement est trop rapide pour être perçu comme tel. Dans cette enveloppe, on a une fonction sinus dont la fréquence est également un multiple de ƒ<sub>0</sub> ; l'enveloppe et la fonction qui y est inscrite ont donc de nombreux « points communs », d'où l'effet harmonieux. === Le tonnetz === [[File:Speculum musicae.png|thumb|right|225px|Euler, ''De harmoniæ veris principiis'', 1774, p. 350.]] En allemand, le terme ''Tonnetz'' (se prononce « tône-netz ») signifie « réseau tonal ». C'est une représentation graphique des notes qui a été imaginée par [[w:Leonhard Euler|Leonhard Euler]] en 1739. Cette représentation graphique peut aider à la mémorisation de certains concepts de l'harmonie. Cependant, son application est très limitée : elle ne concerne que l'intonation juste d'une part, et que les accords parfait des tonalités majeures et mineures naturelles d'autre part. La représentation contenant les douze notes de la musique savante occidentale, on peut bien sûr représenter d'autres objets, comme les accords de septième ou les accords diminués, mais la représentation graphique est alors compliquée et perd son intérêt pédagogique. On part d'une note, par exemple le ''do''. Si on progresse vers la droite, on monte d'une quinte juste, donc ''sol'' ; vers la gauche, on descend d'une quinte juste, donc ''fa''. Si on va vers le bas, on monte d'une tierce majeure, donc ''mi'' ; si on va vers le haut, on descend d'une tierce majeure, donc ''la''♭ ou ''sol''♯ fa — do — sol — ré | | | | la — mi — si — fa♯ | | | | do♯ — sol♯ — ré♯ — si♭ La figure forme donc un filet, un réseau. On voit que ce réseau « boucle » : si on descend depuis le ''do''♯, on monte d'une tierce majeure, on obtient un ''mi''♯ qui est l'enharmonique du ''fa'' qui est en haut de la colonne. Si on va vers la droite à partir du ''ré'', on obtient le ''la'' qui est au début de la ligne suivante. Si on ajoute des diagonales allant vers la droite et le haut « / », on met en évidence des tierces mineures : ''la'' - ''do'', ''mi'' - ''sol'', ''si'' - ''ré'', ''do''♯ - ''mi''… fa — do — sol — ré | / | / | / | la — mi — si — fa♯ | / | / | / | do♯ — sol♯ — ré♯ — si♭ Donc les liens représentent : * | : tierce majeure ; * — : quinte juste ; * / : tierce mineure. [[Fichier:Tonnetz carre accords fr.svg|thumb|Tonnetz avec les accords parfaits. Les notes sont en notation italienne et les accords en notation jazz.]] On met ainsi en évidence des triangles dont un côté est une quinte juste, un côté une tierce majeure et un côté une tierce mineure ; c'est-à-dire que les notes aux sommets du triangle forment un accord parfait majeur (par exemple ''do'' - ''mi'' - ''sol'') : <div style="font-family:courier; background-color:#fafafa"> fa&nbsp;&nbsp;—&nbsp;'''do&nbsp;&nbsp;&nbsp;—&nbsp;sol'''&nbsp;—&nbsp;ré<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;'''|&nbsp;&nbsp;&nbsp;/'''&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;|<br /> la&nbsp;&nbsp;—&nbsp;'''mi'''&nbsp;&nbsp;&nbsp;—&nbsp;si&nbsp;&nbsp;—&nbsp;fa♯<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;/&nbsp;&nbsp;|<br /> do♯&nbsp;—&nbsp;sol♯&nbsp;—&nbsp;ré♯&nbsp;—&nbsp;si♭ </div> ou un accord parfait mineur (''la'' - ''do'' - ''mi''). <div style="font-family:courier; background-color:#fafafa"> fa&nbsp;&nbsp;—&nbsp;'''do'''&nbsp;&nbsp;&nbsp;—&nbsp;sol&nbsp;—&nbsp;ré<br /> |&nbsp;&nbsp;'''/&nbsp;&nbsp;|'''&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;|<br /> '''la&nbsp;&nbsp;—&nbsp;mi'''&nbsp;&nbsp;&nbsp;—&nbsp;si&nbsp;&nbsp;—&nbsp;fa♯<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;/&nbsp;&nbsp;|<br /> do♯&nbsp;—&nbsp;sol♯&nbsp;—&nbsp;ré♯&nbsp;—&nbsp;si♭ </div> Un triangle représente donc un accord, et un sommet représente une note. Si on passe d'un triangle à un triangle voisin, alors on passe d'un accord à un autre accord, les deux accords ayant deux notes en commun. Ceci illustre la notion de « plus court chemin » en harmonie : si on passe d'un accord à un autre en gardant un côté commun, alors on a un mouvement conjoint sur une seule des trois voix. Par rapport à l'harmonie fonctionnelle : les accords sont contigus à leur fonction, par exemple en ''do'' majeur : * fonction de tonique ({{Times New Roman|I}}) : C, A– et E– sont contigus ; * fonction de sous-dominante ({{Times New Roman|IV}}) : F et D– sont contigus ; * fonction de dominante ({{Times New Roman|V}}) : G et B<sup>o</sup> sont contigus. On notera que les triangles d'un schéma ''tonnetz'' ne représentent que des accords parfaits. Pour représenter un accord de quinte diminuée (''si'' - ''ré'' - ''fa'') ou les accords de septième, en particulier l'accord de septième de dominante, il faut étendre le ''tonnetz'' et l'on obtient des figures différentes. Par ailleurs, il est adapté à ce que l'on appelle « l'intonation juste », puisque tous les intervalles sont idéaux. [[Fichier:Tonnetz carre accords etendu fr.svg|vignette|Tonnetz étendu.]] [[Fichier:Tonnetz carre do majeur accords fr.svg|vignette|Tonnetz de la tonalité de ''do'' majeur. La représentation de l'accord de quinte diminuée sur ''si'' (B<sup>o</sup>) est une ligne et non un triangle.]] [[Fichier:Tonnetz carre do mineur accords fr.svg|vignette|Tonnetz des tonalités de ''do'' mineur naturel (haut) et ''do'' mineur harmonique (bas).]] Si l'on étend un peu le réseau : ré♭ — la♭ — mi♭ — si♭ — fa | / | / | / | / | fa — do — sol — ré — la | / | / | / | / | la — mi — si — fa♯ — do♯ | / | / | / | / | do♯ — sol♯ — ré♯ — la♯ — mi♯ | / | / | / | / | mi♯ — do — sol — ré — la on peut donc trouver des chemins permettant de représenter les accords de septième de dominante (par exemple en ''do'' majeur, G<sup>7</sup>) fa / sol — ré | / si et des accords de quinte diminuée (en ''do'' majeur : B<sup>o</sup>) fa / ré / si Une gamme majeure ou mineure naturelle peut se représenter par un trapèze rectangle : ''do'' majeur fa — do — sol — ré | / la — mi — si et ''do'' mineur la♭ — mi♭ — si♭ / | fa — do — sol — ré En revanche, la représentation d'une tonalité nécessite d'étendre le réseau afin de pouvoir faire figurer tous les accords, deux notes sont représentées deux fois. La représentation des tonalités mineures harmoniques prend une forme biscornue, ce qui nuit à l'intérêt pédagogique de la représentation. [[Fichier:Neo-Riemannian Tonnetz.svg|vignette|upright=2|Tonnetz avec des triangles équilatéraux.]] On peut réorganiser le schéma en décalant les lignes, afin d'avoir des triangles équilatéraux. Sur la figure ci-contre (en notation anglo-saxonne) : * si on monte en allant vers la droite « / », on a une tierce mineure ; * si on descend en allant vers la droite « \ », on a une tierce majeure ; * les liens horizontaux « — » représentent toujours des quintes justes * les triangles pointe en haut sont des accords parfaits mineurs ; * les triangles pointe en bas sont des accords parfaits majeurs. On a alors les accords de septième de dominante F / G — D \ / B et de quinte diminuée F / D / B les tonalités majeures F — C — G — D \ / A — E — B et les tonalités mineures naturelles A♭ — E♭ — B♭ / \ F — C — G — D == Notes et références == {{références}} == Voir aussi == === Bibliographie === * {{ouvrage |prénom1=Francesco |nom1=Gasparini |titre=L'Armonico Pratico al Cimbalo |éditeur=Giusseppe Antonio Silvani |lieu=Bologne |année=1772 |lang=it |url=https://imslp.org/wiki/L'Armonico_Pratico_al_Cimbalo_(Gasparini%2C_Francesco)}} === Liens externes === {{wikipédia|Consonance (harmonie tonale)}} {{wikipédia|Disposition de l'accord}} {{wikisource|Petit Manuel d’harmonie}} * {{lien web | url = https://www.apprendrelesolfege.com/chiffrage-d-accords | titre = Chiffrage d'accords (classique) | site = Apprendrelesolfege.com | consulté le = 2020-12-03 }} * {{lien web | url = https://www.coursd-harmonie.fr/introduction/introduction2_le_chiffrage_d_accords.php | titre = Introduction II : Le chiffrage d'accords | site = Cours d'harmonie.fr | consulté le = 2021-12-14 }} * {{lien web | url=https://www.coursd-harmonie.fr/ | titre = Cours d'harmonie en ligne | auteur = Jean-Baptiste Voinet | site=coursd-harmonie.fr | consulté le = 2021-12-20 }} * {{lien web | url=http://e-harmonie.e-monsite.com/ | titre = Cours d'harmonie classique en ligne | auteur = Olivier Miquel | site=e-harmonie | consulté le = 2021-12-24 }} * {{lien web | url=https://fr.audiofanzine.com/theorie-musicale/editorial/dossiers/les-gammes-et-les-modes.html | titre = Les bases de l’harmonie | site = AudioFanzine | date = 2013-07-23 | consulté le = 2024-01-12 }} * {{lien web | url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/l-accord-de-tristan-dechiffre-et-desacralise-2481605 | titre = L’Accord de Tristan déchiffré et désacralisé ! | site = France Musique | date = 2025-10-22 | consulté le = 2025-10-23 }} * {{lien web | url=https://www.youtube.com/watch?v=X62SAnyfQYw | titre = Qu'est-ce que la basse continue dans la musique baroque ? | auteur = Clément Rochefort | site = YouTube/France Musique | date = 2022-05-02 | consulté le = 2026-08-04 }} ---- ''[[../Mélodie|Mélodie]]'' &lt; [[../|↑]] &gt; ''[[../Représentation musicale|Représentation musicale]]'' [[Catégorie:Formation musicale (livre)|Harmonie]] ei9evpukdd0wh6cuvkonhprjfy051nb 773908 773907 2026-10-02T08:33:33Z Cdang 1202 /* Les accords en général */ histoire 773908 wikitext text/x-wiki {{Bases de solfège}} <span style="font-size:25px;">6. Harmonie</span> L'harmonie désigne les notes jouées en même temps, soit plusieurs instruments jouant chacun une note, soit un instrument jouant un accord (instrument dit polyphonique). == Première approche == L'exemple le plus simple d'harmonie est sans doute la chanson en canon : c'est un chant polyphonique, c'est-à-dire à plusieurs voix, chaque voix chantant la même chose en décalé. Prenons par exemple ''Vent frais, vent du matin'' (la version originale est ''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609) : [[Fichier:Vent frais vent du matin.svg|class=transparent|center|Partition de ''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]] [[Fichier:Vent frais vent du matin.midi|vignette|''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]] nous voyons que les voix se superposent de manière « harmonieuse ». Les notes de chaque voix se correspondent point par point (avec un retard), c'est donc un type d'harmonie polyphonique appelé « contrepoint ». Considérons la première note de la mesure 6 pour chaque voix. Nous avons la superposition des notes ''ré''-''fa''-''la'' (du grave vers l'aigu) ; la superposition de notes jouées ou chantées ensembles s'appelle un accord. Cet accord ''ré''-''fa''-''la'' porte le nom « d'accord parfait de ''ré'' mineur » : * « ''ré'' » car la note fondamentale est un ''ré'' ; * « parfait » car il est l'association d'une tierce, ''ré''-''fa'', et d'une quinte juste, ''ré''-''la'' ; * « mineur » car le premier intervalle, ''ré''-''fa'', est une tierce mineure. Considérons maintenant un chant accompagné au piano. Le piano peut jouer plusieurs notes en même temps, il peut jouer des accords. [[Fichier:Au clair de le lune chant et piano.svg|class=transparent|center|Deux premières mesure d’Au clair de la lune.]] [[Fichier:Au clair de le lune chant et piano.midi|vignette|Deux premières mesure d’Au clair de la lune.]] L'accord, les notes à jouer simultanément, sont écrites « en colonne ». Lorsqu'on les énonce, on les lit de bas en haut mais le pianiste les joue en pressant les touches du clavier en même temps, de manière « plaquée ». Le premier accord est composé des notes ''do''-''mi''-''sol'' ; il est appelé « accord parfait de ''do'' majeur » car la note fondamentale est ''do'', qu'il est l'association d'une tierce et d'une quinte juste et que le premier intervalle, ''do''-''mi'', est une tierce majeure. Si l'on lit toutes les notes, de la plus grave à la plus aiguë, nous avons : ''do'' (à la clef de ''fa'', main gauche du piano) - ''do'' - ''mi'' - ''sol'' (main droite du piano) - ''do'' (chant) ; nous voyons que la note ''do'' est jouée plusieurs fois, cela ne change pas la qualité de l'accord. == Consonance et dissonance == [[Fichier:Seconde majeure septieme mineure.svg|vignette|Seconde majeure et son renversement, le septième mineure.]] Les notions de consonance et de dissonance sont culturelles et changent selon l'époque. Nous pouvons néanmoins noter que : * l'accord de seconde, et son renversement la septième, créent des battements, les notes « frottent », c'est un intervalle harmonique dissonant ; mais dans le cas de la septième, comme les notes sont éloignées, le frottement est moins perceptible ; * les accords d'unisson, de tierce, quarte et quinte sonnent agréablement à l'oreille, ils sont consonants. Dans la musique savante européenne, au début du Moyen-Âge, seuls les accords d'unisson, de quarte et de quinte étaient considérés comme consonants, d'où leur qualification de « juste ». La tierce, et son renversement la sixte, étaient perçues comme dissonantes ; à partir de la Renaissance, ces deux intervalles sont considérés comme consonants. L'harmonie joue avec les consonances et les dissonances. Dans un premier temps, les harmonies dissonantes sont utilisées pour créer des tensions qui sont ensuite résolues, on utilise des successions « consonant-dissonant-consonant ». À force d'entendre des intervalles considérés comme dissonants, l'oreille s'habitue et certains finissent par être considérés comme consonants ; c'est ce qui est arrivé à la tierce et à la sixte à la fin du Moyen Âge avec le contrepoint, puis à la septième plus tardivement. Il faut ici aborder la notion d'harmonique des notes. [[File:Harmoniques de do.svg|thumb|Les six premières harmoniques de ''do''.]] Lorsque l'on joue une note, on entend d'autres notes plus aigües et plus faibles ; la note jouée est appelée la « fondamentale » et les notes plus aigües et plus faibles sont les « harmoniques ». C'est cette accumulation d'harmoniques qui donne la couleur au son, son timbre, qui fait qu'un piano ne sonne pas comme un violon. Par exemple, si l'on joue un ''do''<sup>1</sup><ref>Pour la notation des octaves, voir ''[[../Représentation_musicale#Désignation_des_octaves|Représentation musicale &gt; Désignation des octaves]]''.</ref> (fondamentale), on entend le ''do''<sup>2</sup> (une octave plus aigu), puis un ''sol''<sup>2</sup>, puis encore un ''do''<sup>3</sup> plus aigu, puis un ''mi''<sup>3</sup>, puis encore un ''sol''<sup>3</sup>, puis un ''si''♭<sup>3</sup>… Ainsi, puisque lorsque l'on joue un ''do'' on entend aussi un ''sol'' très léger, alors jouer un ''do'' et un ''sol'' simultanément n'est pas choquant. De même pour ''do'' et ''mi''. De là vient la notion de consonance. Le statut du ''si''♭ est plus ambigu. Il fait partie des harmoniques qui sonnent naturellement, mais il forme une seconde descendante avec le ''do'', intervalle dissonant. Par ailleurs, on remarque que le ''si''♭ ne fait pas partie de la gamme de ''do'' majeur, contrairement au ''sol'' et au ''mi''. Pour le jeu sur les dissonances, on peut écouter par exemple la ''Toccata'' en ''ré'' mineur, op. 11 de Sergueï Prokofiev (1912). : {{lien web |url=https://www.youtube.com/watch?v=AVpnr8dI_50 |titre=Yuja Wang Prokofiev Toccata |site=YouTube |date=2019-02-26 |consulté le=2021-12-19}} == Contrepoint == Dans le chant grégorien, la notion d'accord n'existe pas. L'harmonie provient de la superposition de plusieurs mélodies, notamment dans ce que l'on appelle le « contrepoint ». Le terme provient du latin ''« punctum contra punctum »'', littéralement « point par point », et désigne le fait que les notes de chaque voix se correspondent. L'exemple le plus connu de contrepoint est le canon, comme ''Frère Jacques'' ou bien ''Vent frais, vent du matin'' présenté ci-dessus : chaque note d'un couplet correspond à une note du couplet précédent. Certains morceaux sont bâtis sur une écriture « en miroir » : l'ordre des notes est inversé entre les deux voix, ou bien les intervalles sont inversés (« mouvement contraire » : une tierce montante sur une voix correspond à une tierce descendante sur l'autre). On peut également citer le « mouvement oblique » (une des voix, le bourdon, chante toujours la même note) et le mouvement parallèle (les deux voix chantent le même air mais transposé, l'une est plus aiguë que l'autre). Nous reproduisons ci-dessous le début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel. [[Fichier:Haendel Sonate en trio re mineur debut canon.svg | vignette | center | upright=2 | Début du second ''Allergo'' de la sonate en trio en ''ré'' mineur de Haendel.]] [[Fichier:Haendel Sonate en trio re mineur debut.midi | vignette | Début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.]] Nous avons mis en évidence la construction en canon avec des encadrés de couleur : sur les quatre premières mesures, nous voyons trois motifs repris alternativement par une voix et par l'autre. Ce type de procédé est très courant dans la musique baroque. Les procédés du contrepoint s'appliquent également à la danse : * unisson : les danseurs et danseuses font les mêmes gestes en même temps ; * répétition : le fait de répéter une série de gestes, une « phrase dansante » ; * canon : les gestes sont faits avec un décalage régulier d'un danseur ou d'une danseuse à l'autre ; * cascade : forme de canon dans laquelle le décalage est très petit ; * contraste : deux danseur·euses, ou deux groupes, ont des gestuelles très différentes ; * accumulation : la gestuelle se complexifie par l'ajout d'éléments au fur et à mesure ; ou bien le nombre de danseur·euses augmente ; * dialogue : les gestes de danseur·euses ou de groupes se répondent ; * contre-point : la gestuelle d'un ou une danseuse se superpose à la gestuelle d'un groupe ; * lâcher-rattraper : les danseurs et danseuses alternent danse à l'unisson et gestuelles indépendantes. : {{lien web | url=https://www.youtube.com/watch?v=wgblAOzedFc | titre=Les procédés de composition en danse | auteur= Doisneau Sport TV | site=YouTube | date=2020-03-16 | consulté le=2021-01-21 }} {{...}} == Ostinato == L'ostinato, mot italien signifiant « obstiné », c'est le fait de jouer en boucle un passage. L'ostinato peut ainsi former une structure autour de laquelle peut se développer le morceau. Lorsque l'ostinato est joué par l'instrument de basse (basson, violoncelle, viole de gambe basse, flûte à bec basse, main gauche du piano…), on parle de basse obstinée. Les grilles d'accord en jazz sont des ostinatos (ou ostinati) joués par la section rythmique. Exemples : * {{lien web |url=https://www.youtube.com/watch?v=QZpGe5rNJkI&t=41s |titre=Epic Patty Cake Song (I'll Think Of You) |auteur= Kurt Hugo Schneider |site=YouTube |date=2014-08-12 |consulté le=2026-01-24}} : un ostinato de percussions corporelles ; * {{lien web |url=https://www.youtube.com/watch?v=Kv-37-nXC48 |titre=Pow Wow ''Le lion est mort ce soir'' Les Victoires de la Musique 1993 |auteur=Solomon Linda |date=2015-02-02 |consulté le=2026-01-24}} : dans ce chant ''a capella'' (sans accompagnement d'instrument), les voix de basse forment une basse obstinée, sur les syllabes « oëap » durant le couplet et « awimbowé » sur le refrain ; * {{lien web |url=https://www.youtube.com/watch?v=JvNQLJ1_HQ0 |titre=Pachelbel Canon in D Major - the original and best version. |auteur=Voices of Music |site=YouTube |date=2008-09-02 |consulté le=2026-01-24}} : le ''Canon de Pachelbel de ''ré'' mineur'' (''Canon per tre Violini e Basso'', Johann Pachelbel, vers 1680) est construit sur une basse obstinée de huit mesures. == Les accords en général == Initialement, on a des chants polyphoniques, des voix qui chantent chacune une mélodie, les mélodies se mêlant. On remarque que certaines superpositions de notes sonnent de manière plus ou moins agréables, consonantes ou dissonantes. On en vient alors à associer ces notes, c'est-à-dire à considérer dès le départ la superposition de ces notes et non pas la rencontre de ces notes au gré des mélodies. Ces groupes de notes superposées forment les accords. En Europe, cette notion apparaît vers le {{pc|xiv}}<sup>e</sup> siècle avec notamment la ''[[wikipedia:fr:Messe de Notre Dame|Messe de Notre Dame]]'' de Guillaume de Machaut (vers 1360-1365). À l'origine, dans les chants polyphoniques, les voix sont d'égale importance. Puis, au cours du temps, une des voix devient plus importante, le « cantus firmus » ; initialement, il s'agit de la voix intermédiaire, le « ténor » ou « taille », puis c'est la voix la pus aigüe, le « soprano » ou « dessus ». À la Renaissance, au {{pc|xvi}}{{e}} siècle, apparaît la notion de « monodie accompagnée » : la voix principale devient la voix mélodique, et les autres voix servent à l'accompagner. À l'époque baroque, au {{pc|xvii}}{{e}} siècle, apparaît la notion d'accord, notes sonnant simultanément, avec une notation simplifiée, la « basse chiffrée » : a lieu d'écrire toutes les notes de l'accord, on écrit la basse et des chiffres indiquent quelles sont les autres notes à jouer. La notion « d'accord parfait » est consacrée par [[wikipedia:fr:Jean-Philippe Rameau|Jean-Philippe Rameau]] dans son ''Traité de l'harmonie réduite à ses principes naturels'', publié en 1722. === Qu'est-ce qu'un accord ? === Un accord est un ensemble d'au minimum trois notes jouées en même temps. « Jouées » signifie qu'il faut qu'à un moment donné, elles sonnent en même temps, mais le début ou la fin des notes peut être à des instants différents. Considérons que l'on joue les notes ''do'', ''mi'' et ''sol'' en même temps. Cet accord s'appelle « accord de ''do'' majeur ». En musique classique, on lui adjoint l'adjectif « parfait » : « accord parfait de ''do'' majeur ». Nous représentons ci-dessous trois manière de faire l'accord : avec trois instruments jouant chacun une note : [[Fichier:Do majeur trois portees.svg|class=transparent|center|Accord de ''do'' majeur avec trois instruments différents.]] Avec un seul instrument jouant simultanément les trois notes : [[Fichier:Chord C.svg|class=transparent|center|Accord de ''do'' majeur joué par un seul instrument.]] L'accord tel qu'il est joué habituellement par une guitare d'accompagnement : [[Fichier:Do majeur guitare.svg|class=transparent|center|Accord de ''do'' majeur à la guitare.]] Pour ce dernier, nous représentons le diagramme indiquant la position des doigts sur le manche au dessus de la portée et la tablature en dessous. Ici, c'est au total six notes qui sont jouées : ''mi'' grave, ''do'' médium, ''mi'' médium, ''sol'' médium, ''do'' aigu, ''mi'' aigu. Mais il s'agit bien des trois notes ''do'', ''mi'' et ''sol'' jouées à des octaves différentes. Nous remarquons également que la note de basse (la note la plus grave), ''mi'', est différente de la note fondamentale (celle qui donne le nom à l'accord), ''do'' ; l'accord est dit « renversé » (voir plus loin). === Comment joue-t-on un accord ? === Les notes ne sont pas forcément jouées en même temps ; elles peuvent être « égrainées », jouée successivement, ce que l'on appelle un arpège. La partition ci-dessous montre six manières différentes de jouer un accord de ''la'' mineur à la guitare, plaqué puis arpégé. [[Fichier:La mineur differentes executions.svg|class=transparent|center|Différentes exécution de l'accord de do majeur à la guitare.]] [[Fichier:La mineur differentes executions midi.midi|vignette|Différentes exécution de l'accord de la mineur à la guitare.]] Vous pouvez écouter l'exécution de cette partition avec le lecteur ci-contre. Seuls les instruments polyphoniques peuvent jouer les accords plaqués : instruments à clavier (clavecin, orgue, piano, accordéon), les instruments à plusieurs cordes pincées (harpe, guitare ; violon, alto, violoncelle et contrebasse joués en pizzicati). Les instruments à corde frottés de la famille du violon peuvent jouer des notes par deux à l'archet mais pas plus du fait de la forme bombée du chevalet ; cependant, un mouvement rapide permet de jouer les quatre cordes de manière très rapprochée. Les instruments à percussion de type xylophone ou le tympanon permettent de jouer jusqu'à quatre notes simultanément en tenant deux baguettes (mailloches, maillets) par main. Tous les instruments peuvent jouer des arpèges même si, dans le cas des instruments monodiques, les notes ne continuent pas à sonner lorsque l'on passe à la note suivante. L'arpège peut être joué par l'instrument de basse (basson, violoncelle, contrebasse, guitare basse, pédalier de l'orgue…), notamment dans le cas d'une basse continue ou d'une ''{{lang|en|walking bass}}'' (« basse marchante » : la basse joue des noires, donnant ainsi l'impression qu'elle marche). En jazz, et spécifiquement au piano, on a recours au ''{{lang|en|voicing}}'' : on choisit la manière dont on organise les notes pour donner une couleur spécifique, ou bien pour créer une mélodie en enchaînant les accords. Il est fréquent de ne pas jouer toutes les notes : si on n'en garde que deux, ce sont la tierce et la septième, car ce sont celles qui caractérisent l'accord (selon que la tierce est mineure ou majeure, que la septième est majeure ou mineure), et la fondamentale est en général jouée par la contrebasse ou guitare basse. {{clear}} === Classes d'accord === [[Fichier:Intervalles harmoniques accords classes.svg|vignette|upright=1.5|Intervalles harmoniques dans les accords classés de trois, quatre et cinq notes.]] Un accord composé d'empilement de tierces est appelé « accord classé ». En musique tonale, c'est-à-dire la musique fondée sur les gammes majeures ou mineures (cas majoritaire en musique classique), on distingue trois classes d'accords : * les accords de trois notes, ou triades, ou accords de quinte ; * les accords de quatre notes, ou accords de septième ; * les accords de cinq notes, ou accords de neuvième. En empilant des tierces, si l'on part de la note fondamentale, on a donc de intervalles de tierce, quinte, septième et neuvième. En musique tonale, les accords avec d'autres intervalles (hors renversement, voir ci-après), typiquement seconde, quarte ou sixte, sont considérés comme des transitions entre deux accords classés. Ils sont appelés, selon leur utilisation, « accords à retard » (en anglais : ''{{lang|en|suspended chord}}'', accord suspendu) ou « appoggiature » (note « appuyée », étrangère à l'harmonie). Voir aussi plus loin la notion de note étrangère. === Renversements d'accords === [[File:Accord do majeur renversements.svg|thumb|Accord parfait de do majeur et ses renversements.]] [[Fichier:Progression dominante renverse parfait do majeur.svg|vignette|upright=0.6|Progression accord de dominante renversé → accord parfait en ''do'' majeur.]] Un accord classé est donc un empilement de tierces. Si l'on change l'ordre des notes, on a toujours le même accord mais il est fait avec d'autres intervalles harmoniques. Par exemple, l'accord parfait de ''do'' majeur dans son état fondamental, c'est-à-dire non renversé, s'écrit ''do'' - ''mi'' - ''sol''. Sa note fondamentale, ''do'', est aussi se note de basse. Si maintenant on prend le ''do'' de l'octave supérieure, l'accord devient ''mi - sol - do'' ; c'est l'empilement d'une tierce ''(mi - sol)'' et d'une quarte ''(sol - do)'', soit la superposition d'une tierce ''(mi - sol)'' et d'une sixième ''(mi - do)''. C'est le premier renversement de l'accord parfait de ''do'' majeur ; la fondamentale est toujours ''do'' mais la basse est ''mi''. Le second renversement est ''sol - do - mi''. L'utilisation de renversement peut faciliter l'exécution de la progression d'accord. Par exemple, en tonalité ''do'' majeur, si l'on veut passer de l'accord de dominante ''sol - si - ré'' à l'accord parfait ''do - mi - sol'', alors on peut utiliser le second renversement de l'accord de dominante : ''ré - sol - si'' → ''do - mi - sol''. Ainsi, la basse descend juste d'un ton ''(ré → do)'' et sur un piano, la main reste globalement dans la même position. Le renversement d'un accord permet également de respecter certaines règles de l'harmonie classique, notamment éviter que des voix se suivent strictement (« mouvement parallèle »), ce qui aurait un effet de platitude. De manière générale, la notion de renversement permet deux choses : * d'enrichir l'œuvre : pour créer une harmonie donnée (c'est-à-dire des sons sonnant bien ensemble), nous avons plus de souplesse, nous pouvons organiser ces notes comme nous le voulons selon les voix ; * de simplifier l'analyse : quelle que soit la manière dont sont organisées les notes, cela nous ramène à un même accord. {{citation bloc|Or il, y a plusieurs manières de jouer un accord, selon que l'on aborde par la première note qui le constitue, ''do mi sol'', la deuxième, ''mi sol do'', ou la troisième note, ''sol do mi''. Ce sont les renversements, [que Rameau] va classer en différentes combinaisons d'une seule matrice. Faisant cela, Rameau divise le nombre d'accords [de septième] par quatre. Il simplifie, il structure […].|{{ouvrage|prénom1=André |nom1=Manoukian |titre=Sur les routes de la musique |éditeur=Harper Collins |année=2021 |passage=54 |isbn=979-1-03391201-9}} }} {{clear}} [[File:Plusieurs realisation 1er renversement doM.svg|thumb|Plusieurs réalisation du premier renversement de l'accord de ''do'' majeur.]] Notez que # Les notes peuvent être répétées plusieurs fois, à différentes octaves. # Dans la notion de renversement, seule importe en fait la note de basse. Ainsi, les accords ''mi-sol-do'', ''mi-do-sol'', ''mi-do-mi-sol'', ''mi-sol-mi-do''… sont tous une déclinaison du premier renversement de ''do-mi-sol'' et ils seront abrégés de la même manière (''mi''<sup>6</sup> en musique classique ou C/E en musique populaire et jazz, voir plus bas). {{clear}} == Notation des accords de trois notes == Les accords de trois notes sont appelés « accords de quinte » en classique, et « triades » en jazz. [[Fichier:Progression dominante renverse parfait do majeur chiffrage.svg|vignette|upright=0.7|Chiffrage du second renversement d'un accord de ''sol'' majeur et d'un accord de ''do'' majeur : notation en musique populaire et jazz (haut) et notation de basse chiffrée (bas).]] Les accords sont construits de manière systématique. Nous pouvons donc les représenter de manière simplifiée. Cette notation simplifiée des accords est appelée « chiffrage ». Reprenons la progression d'accords ci-dessus : « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » dans la tonalité de ''do'' majeur. On utilise en général trois notations différentes : * en musique populaire, jazz, rock… un accord est désigné par sa note fondamentale ; ici donc, les accords sont notés « ''sol'' - ''do'' » ou, en notation anglo-saxonne, « G - C » ;<br /> comme le premier accord est renversé, on indique la note de basse après une barre, la progression d'accords est donc chiffrée '''« ''sol''/''ré'' - ''do'' »''' ou '''« G/D - C »''' ;<br /> il s'agit ici d'accords composés d'une tierce majeure et d'une quinte juste ; si les accords sont constitués d'intervalles différents, nous ajoutons un symbole après la note : « m » ou « – » si la tierce est mineure, « dim » ou « ° » si la quinte est diminuée ; * en musique classique, on utilise la notation de « basse chiffrée » (utilisée notamment pour noter la basse continue en musique baroque) : on indique la note de basse sur la portée et on lui adjoint l'intervalle de la fondamentale à la note la plus haute (donc ici respectivement 6 et 5, puisque ''sol''-''si'' est une sixte et ''do''-''sol'' est une quinte), étant sous-entendu que l'on a des empilements de tierce en dessous ; mais dans le cas du premier accord, le premier intervalle n'est pas une tierce, mais une quarte ''(ré''-''sol)'', on note donc '''« ''ré'' <sup>6</sup><sub>4</sub> - ''do'' <sup>5</sup> »'''<ref>quand on ne dispose pas de la notation en supérieur (exposant) et inférieur (indice), on utilise parfois une notation sous forme de fraction : ''sol'' 6/4 et ''do'' 5/.</ref> ; * lorsque l'on fait l'analyse d'un morceau, on s'attache à identifier la note fondamentale de l'accord (qui est différente de la basse dans le cas d'un renversement) ; on indique alors le degré de la fondamentale : '''« {{Times New Roman|V<sup>6</sup><sub>4</sub> - I<sup>5</sup>}} »'''. La notation de basse chiffrée permet de construire l'accord à la volée : * on joue la note indiquée (basse) ; * s'il n'y a pas de 2 ni de 4, on lui ajoute la tierce ; * on ajoute les intervalles indiqués par le chiffrage. La notation de musique jazz oblige à connaître la composition des différents accords, mais une fois que ceux-ci sont acquis, il n'y a pas besoin de reconstruire l'accord. La notation de basse chiffrée avec les chiffres romains n'est pas utilisée pour jouer, mais uniquement pour analyser ; Sur les partitions avec basse chiffrée, il y a simplement les chiffrages indiqués au-dessus de la partie de basse. Le chiffrage avec le degré en chiffres romains présente l'avantage d'être indépendant de la tonalité et donc de se concentrer sur la fonction de l'accord au sein de la tonalité. Par exemple, ci-dessous, nous pouvons parler de la progression d'accords « {{Times New Roman|V - I}} » de manière générale, cette notation étant valable quelle que soit la tonalité. [[File:Progression dominante renverse parfait do majeur chiffrage basse continue.svg|thumb|Chiffrage en notation basse chiffrée de la progression d'accords « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » en do majeur.]] {{note|En notation de base continue avec fondamentale en chiffres romains, la fondamentale est toujours indiquée ''sous'' la portée de la partie de basse. Les intervalles sont indiqués au-dessus de la portée de la partie de basse ; lorsque l'on fait une analyse, on peut ayssi les indiquer à côté du degré en chiffres romains, donc sous la portée de la basse.}} {{note|En notation rock, le 5 en exposant indique un accord incomplet avec uniquement la fondamentale et la quinte, un accord sans tierce appelé « accord de puissance » ou ''{{lang|en|power chord}}''. Par exemple, C<sup>5</sup> est l'accord ''do-sol''.}} {{clear}} [[Fichier:Accords parfait do majeur basse chiffree fondamental et renverse.svg|vignette|upright=2.5|Chiffrage de l'accord parfait de ''do'' majeur en basse chiffrée, à l'état fondamental et ses renversements.]] Concernant les accords parfaits en notation de basse chiffrée : * un accord parfait à l'état fondamental est chiffré « <sup>5</sup> » ; on l'appelle « accord de quinte » ; * le premier renversement est chiffré « <sup>6</sup> » (la tierce est implicite) ; on l'appelle « accord de sixte » ; * le second renversement est noté « <sup>6</sup><sub>4</sub> » ; on l'appelle « accord de sixte et de quarte » (ou bien « de quarte et de sixte »). Par exemple, pour l'accord parfait de ''do'' majeur : * l'état fondamental ''do''-''mi''-''sol'' est noté ''do''<sup>5</sup> ; * le premier renversement ''mi''-''sol''-''do'' est noté ''mi''<sup>6</sup> ; * le second renversement ''sol''-''do''-''mi'' est noté ''sol''<sup>6</sup><sub>4</sub>. Il y a une exception : l'accord construit sur la sensible (7{{e}} degré) contient une quinte diminuée et non une quinte juste. Le chiffrage est donc différent : * l'état fondamental ''si''-''ré''-''fa'' est noté ''si''<sup><s>5</s></sup> (cinq barré), « accord de quinte diminuée » ; * le premier renversement ''ré''-''fa''-''si'' est noté ''ré''<sup>+6</sup><sub>3</sub>, « accord de sixte sensible et tierce » ; * le second renversement ''fa''-''si''-''ré'' est noté ''fa''<sup>6</sup><sub>+4</sub>, « accord de sixte et quarte sensible ». Par ailleurs, on ne considère pas qu'il est fondé sur la sensible, mais sur la dominante. Si certains indiquent le degré {{Times New Roman|VII}}, d'autres indiquent le degré {{Times New Roman|V}} en mettant des guillemets, « {{Times New Roman|“V”}} ». Donc selon l'état, le chiffrage est {{Times New Roman|“V”<sup><s>5</s></sup>}}, {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}} ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}. En notation jazz, on ajoute « dim », « <sup>o</sup> » ou bien « <sup>♭5</sup> » au chiffrage, ici : B dim, B<sup>o</sup> ou B<sup>♭5</sup> pour l'état fondamental. Pour les renversements : B dim/D et B dim/F ; ou bien B<sup>o</sup>/D et B<sup>o</sup>/F ; ou bien B<sup>♭5</sup>/D et B<sup>♭5</sup>/F. {{clear}} [[Fichier:Accords basse chiffree basse do fondamental et renverses.svg|vignette|upright=2|Basse chiffrée : accords de quinte, de sixte et de sixte et de quarte ayant pour basse ''do''.]] Et concernant les accords ayant pour basse ''do'' en tonalité de ''do'' majeur : * l'accord ''do''<sup>5</sup> est un accord à l'état fondamental, c'est donc l'accord ''do''-''mi''-''sol'' (sa fondamentale est ''do'') ; * l'accord ''do''<sup>6</sup> est le premier renversement d'un accord, c'est donc l'accord ''do''-''mi''-''la'' (sa fondamentale est ''la'') ; * l'accord ''do''<sup>6</sup><sub>4</sub> est le second renversement d'un accord, c'est donc l'accord ''do''-''fa''-''la'' (sa fondamentale est ''fa''). {{clear}} == Notes étrangères == La musique européenne s'appuie essentiellement sur des accords parfaits, c'est-à-dire fondés sur une tierce majeure ou mineure, et une quinte juste. Il arrive fréquemment qu'un accord ne soit pas un accord parfait. Les notes qui font partie de l'accord parfait sont appelées « notes naturelles » et la note qui n'en fait pas partie est appelée « note étrangère ». Il existe plusieurs types de notes étrangères : * anticipation : la note étrangère est une note naturelle de l'accord suivant ; * appogiature : note d'ornementation qui se résout par mouvement conjoint, c'est-à-dire qu'elle est suivie par une note située juste au-dessus ou en dessous (seconde ascendante ou descendante) qui est, elle, une note naturelle ; * broderie : on part d'une note naturelle, on monte ou on descend d'une seconde, puis on revient sur la note naturelle ; * double broderie : on part d'une note naturelle, on joue la note du dessus puis la note du dessous avant de revenir à la note naturelle ; ou bien on joue la note du dessous puis la note du dessus ; * échappée : note étrangère n'appartenant à aucune des autres catégories ; * note de passage : mouvement conjoint allant d'une note naturelle d'un accord à une note naturelle de l'accord suivant ; * pédale : la note de basse reste la même pendant plusieurs accords successifs ; * retard : la note étrangère est une note naturelle de l'accord précédent. Les notes étrangères ne sont pas chiffrées. [[File:Notes etrangeres accords.svg|center|Différents types de notes étrangères.]] {{note|Les anglophones distinguent deux types de retard : la ''{{lang|en|suspension}}'' est résolue vers le haut (le mouvement est ascendant), le ''{{lang|en|retardation}}'' est résolu vers le bas (le mouvement est descendant).}} == Principaux accords == Les trois principaux accords sont : * l'accord parfait majeur : il est construit sur les degrés {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (médiante) et {{Times New Roman|V}} (dominante) d'une gamme majeure ; il est noté {{Times New Roman|I}}<sup>5</sup>, {{Times New Roman|IV}}<sup>5</sup>, {{Times New Roman|V}}<sup>5</sup> ; * l'accord parfait mineur : il est construit sur les degrés {{Times New Roman|I}} (tonique) et {{Times New Roman|IV}} (sous-tonique) d'une gamme mineure harmonique ; il est également noté {{Times New Roman|I}}<sup>5</sup> et {{Times New Roman|IV}}<sup>5</sup>, les anglo-saxons le notent {{Times New Roman|i}}<sup>5</sup> et {{Times New Roman|iv}}<sup>5</sup> (la minuscule indiquant le caractère mineur) ; * l'accord de septième de dominante : il est construit sur le degré {{Times New Roman|V}} (dominante) d'une gamme majeure ou mineure harmonique ; il est noté {{Times New Roman|V}}<sup>7</sup><sub>+</sub>. On peut trouver ces trois accords sur d'autres degrés, et il existe d'autre types d'accords. Nous verrons cela plus loin. {| class="wikitable" |+ Constitution des principaux accords — Dénomination classique |- ! scope="col" | Accord ! scope="col" | 1<sup>er</sup> intervalle ! scope="col" | 2<sup>e</sup> intervalle ! scope="col" | 3<sup>e</sup> intervalle |- ! scope="row" | Accord parfait majeur | tierce majeure (3M) || quinte juste (5J) || — |- ! scope="row" | Accord parfait mineur | tierce mineure (3m) || quinte juste (5J) || — |- ! scope="row" | Accord de septième de dominante | tierce majeure (3M) || quinte juste (5J) || septième mineure (7m) |} {| class="wikitable" |+ Constitution des principaux accords — Dénomination jazz |- ! scope="col" | Accord ! scope="col" | 1<sup>er</sup> intervalle ! scope="col" | 2<sup>e</sup> intervalle ! scope="col" | 3<sup>e</sup> intervalle |- ! scope="row" | Triade majeure | tierce majeure (3M) || quinte juste (5J) || — |- ! scope="row" | Triade mineure | tierce mineure (3m) || quinte juste (5J) || — |- ! scope="row" | Accord de septième | tierce majeure (3M) || quinte juste (5J) || septième mineure (7m) |} {| border="0" |- | [[Fichier:Accord do majeur arpege puis plaque.midi | Accord parfait de ''do'' majeur (C).]] || [[Fichier:Accord do mineur arpege puis plaque.midi | Accord parfait de ''do'' mineur (Cm).]] || [[Fichier:Accord do septieme arpege puis plaque.midi | Accord de septième de dominante de ''fa'' majeur (C<sup>7</sup>).]] |- | Accord parfait<br /> de ''do'' majeur (C). || Accord parfait<br /> de ''do'' mineur (Cm). || Accord de septième de dominante<br /> de ''fa'' majeur (C<sup>7</sup>). |} '''Rappel :''' * la tierce mineure est composée d'un ton et demi (1 t ½) ; * la tierce majeur est composée de deux tons (2 t) ; * la quinte juste a la même altération que la fondamentale, sauf lorsque la fondamentale est ''si'' (la quinte juste est alors ''fa''♯) ; * la septième mineure est le renversement de la seconde majeure (1 t). [[File:Renversements accords pft fa maj basse chiffree.svg|thumb|Renversements de l'accord parfait de ''fa'' majeur, et la notation de basse chiffrée.]] [[File:Renversements accord sept de dom fa maj basse chiffree.svg|thumb|Renversements de l'accord de septième de dominante de ''fa'' majeur, et la notation de basse chiffrée.]] {| class="wikitable" |+ Notation des principaux accords en musique classique |- ! scope="col" | Accord ! scope="col" | État<br /> fondamental ! scope="col" | Premier<br /> renversement ! scope="col" | Deuxième<br /> renversement ! scope="col" | Troisième<br /> renversement |- ! scope="row" | Accord parfait | {{Times New Roman|I<sup>5</sup>}}<br/> acc. de quinte || {{Times New Roman|I<sup>6</sup>}}<br :> acc. de sixte || {{Times New Roman|I<sup>6</sup><sub>4</sub>}}<br /> acc. de quarte et de sixte || — |- ! scope="row" | Accord de septième<br /> de dominante | {{Times New Roman|V<sup>7</sup><sub>+</sub>}}<br /> acc.de septième de dominante || {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}<br />acc. de sixte et quinte diminuée || {{Times New Roman|V<sup>+6</sup>}}<br />acc. de sixte sensible || {{Times New Roman|V<sup>+4</sup>}}<br />acc. de quarte sensible<br />acc. de triton |} {| class="wikitable" |+ Notation des principaux accords en jazz |- ! scope="col" | Accord ! scope="col" | Chiffrage ! scope="col" | Renversements |- ! scope="row" | Triade majeure | X | rowspan="3" | Les renversements se notent en mettant la basse après une barre de fraction, par exemple pour la triade de ''do'' majeur : * état fondamental : C ; * premier renversement : C/E ; * second renversement : C/G. |- ! scope="row" | Triade mineure | Xm, X– |- ! scope="row" | Septième | X<sup>7</sup> |} {{clear}} Dans le cas d'un accord de septième de dominante, le nom de l'accord change selon que l'on est en musique classique ou en jazz : en musique classique, on donne le nom de la tonalité alors qu'en jazz, on donne le nom de la fondamentale. Ainsi, l'accord appelé « septième de dominante de ''do'' majeur » en musique classique, est appelé « ''sol'' sept » (G<sup>7</sup>) en jazz : la dominante (degré {{Times New Roman|V}}, dominante) de la tonalité de ''do'' majeur est la note ''sol''. Comment appelle-t-on en musique classique l'accord appelé « ''do'' sept » (C<sup>7</sup>) en jazz ? Les tonalités dont le ''do'' est la dominante sont les tonalités de ''fa'' majeur (''si''♭ à la clef) et de ''fa'' mineur harmonique (''si''♭, ''mi''♭, ''la''♭ et ''ré''♭ à la clef et ''mi''♮ accidentel). Il s'agit donc de l'accord de septième de dominante des tonalités de ''fa'' majeur et ''fa'' mineur harmonique. {| class="wikitable" |+ Accords fréquents pour quelques la tonalités majeures ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Accord parfait<br />{{Times New Roman|I<sup>5</sup>}} ! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}} |- |''Do'' majeur || || C<br />''do-mi-sol'' || G7<br />''sol-si-ré-fa'' |- |''Sol'' majeur || ''fa''♯ || G<br />''sol-si-ré'' || D7<br />''ré-fa''♯''-la-do'' |- |''Ré'' majeur || ''fa''♯, ''do''♯ || D<br />''ré-fa''♯''-la'' || A7<br />''la-do''♯''-mi-sol'' |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A<br />''la-do''♯''-mi'' || E7<br />''mi-sol''♯''-si-ré'' |- | ''Fa'' majeur || ''si''♭ || F<br />''fa-la-do'' || C7<br />''do-mi-sol-si''♭ |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭<br />''si''♭''-ré-fa'' || F7<br />''fa-la-do-mi''♭ |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭<br />''mi''♭''-sol-si''♭ || B♭7<br />''si''♭''-ré-fa-la''♭ |} {| class="wikitable" |+ Accords fréquents pour quelques la tonalités mineures harmoniques ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Accord parfait<br />{{Times New Roman|i<sup>5</sup>}} ! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}} |- |''La'' mineur<br />harmonique || || Am, A–<br />''la-do-mi'' || E7<br />''mi-sol''♯''-si-ré'' |- |''Mi'' mineur<br />harmonique || ''fa''♯ || Em, E–<br />''mi-sol-si'' || B7<br />''si-ré''♯''-fa''♯''-la'' |- |''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || Bm, B–<br />''si-ré-fa''♯ || F♯7<br />''fa''♯''la''♯''-do''♯''-mi'' |- |''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || F♯m, F♯–<br />''fa''♯''-la-do''♯ || C♯7<br />''do''♯''-mi''♯''-sol''♯''-si'' |- | ''Ré'' mineur<br />harmonique || ''si''♭ || Dm, D–<br />''ré-fa-la'' || A7<br />''la-do''♯''-mi-sol'' |- | ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || Gm, G–<br />''sol-si''♭''-ré'' || D7<br />''ré-fa''♯''-la-do'' |- | ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || Cm, C–<br />''do-mi''♭''-sol'' || G7<br />''sol-si''♮''-ré-fa'' |} {{clear}} == Accords sur les degrés d'une gamme == === Harmonisation d'une gamme === [[Fichier:Accord trois notes gamme do majeur chiffre.svg|vignette|upright=1.2|Accords de trois note sur la gamme de ''do'' majeur, chiffrés.]] On peut ainsi construire une triade par degré d'une gamme. Pour une gamme majeure, les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|V<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|II<sup>5</sup>}}, {{Times New Roman|III<sup>5</sup>}}, {{Times New Roman|VI<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|ii<sup>5</sup>}}, {{Times New Roman|iii<sup>5</sup>}}, {{Times New Roman|vi<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}. Les accords ont tous une quinte juste à l'exception de l'accord {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} qui a une quinte diminuée, raison pour laquelle le « 5 » est barré. C'est un accord dit « de quinte diminuée ». En jazz, l'accord diminué est noté « dim », « ° », « m<sup>♭5</sup> » ou « <sup>–♭5</sup> ». Nous avons donc trois types d'accords (dans la notation jazz) : X (triade majeure), Xm (triade mineure) et X° (triade diminuée), la lettre X remplaçant le nom de la note fondamentale. {{clear}} [[Fichier:Accord trois notes gamme la mineur chiffre.svg|vignette|upright=1.2|Accords de trois notes sur une gamme de ''la'' mineur harmonique, chiffrés.]] Pour une gamme mineure harmonique, les accords {{Times New Roman|III<sup>+5</sup>}}, {{Times New Roman|V<sup>♯</sup>}} et {{Times New Roman|VI<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|II<sup><s>5</s></sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|i<sup>5</sup>}}, {{Times New Roman|ii<sup><s>5</s></sup>}}, {{Times New Roman|iv<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}. Les accords {{Times New Roman|ii<sup><s>5</s></sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} ont une quinte diminuée ; ce sont des accords dits « de quinte diminuée ». L'accord {{Times New Roman|III<sup>+5</sup>}} a une quinte augmentée ; le signe « plus » indique que la note de cinquième, le ''sol'' dièse, est la sensible. En jazz, l'accord est noté « aug » ou « <sup>+</sup> ». Les autres accords ont une quinte juste. Aux trois accords générés par une gamme majeure (X, Xm et X°), nous voyons ici apparaître un quatrième type d'accord : la triade augmentée X<sup>+</sup>. Nous remarquons que des gammes ont des accords communs. Par exemple, l'accord {{Times New Roman|ii<sup>5</sup>}} de ''do'' majeur est identique à l'accord {{Times New Roman|iv<sup>5</sup>}} de ''la'' mineur (il s'agit de l'accord Dm). Quel que soit le mode, les accords construits sur la sensible (accord de quinte diminuée) sont rarement utilisés. S'ils le sont, c'est en tant qu'accord de septième de dominante sans fondamentale (voir ci-après). C'est la raison pour laquelle le chiffrage indique le degré {{Times New Roman|V}} entre guillemets, et non pas le degré {{Times New Roman|VII}} (mais pour des raisons de clarté, nous l'indiquons entre parenthèses au début). En mode mineur, l'accord de quinte augmentée {{Times New Roman|iii<sup>+5</sup>}} est très peu utilisé (voir plus loin ''[[#Progression_d'accords|Progression d'accords]]''). C'est un accord considéré comme dissonant. On voit que : * un accord parfait majeur peut appartenir à cinq gammes différentes ;<br /> par exemple l'accord parfait de ''do'' majeur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> degré de la gamme de ''do'' majeur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' mineur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''mi'' mineur ; * un accord parfait mineur peut appartenir à cinq gammes différentes ;<br />par exemple l'accord parfait de ''la'' mineur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> de la gamme de ''la'' mineur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''mi'' mineur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|III}}<sup>e</sup> degré de ''fa'' majeur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''do'' majeur ; * un accord de quinte diminuée peut appartenir à trois gammes différentes ;<br />par exemple, l'accord de quinte diminuée de ''si'' est l'accord construit sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' majeur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''la'' mineur et sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' mineur ; * un accord de quinte augmentée (à l'état fondamental) ne peut appartenir qu'à une seule gamme ;<br /> par exemple, l'accord de quinte augmentée de ''do'' est l'accord construit sur le {{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur. {| class="wikitable" |+ Notation jazz des triades |- | rowspan="2" colspan="2" | ! scope="col" colspan="2" | Tierce |- ! scope="col" | 3m ! scope="col" | 3M |- ! rowspan="3" | Quinte ! scope="row" | 5d | Xᵒ, X<sub>m</sub><sup>(♭5)</sup> || |- ! scope="row" | 5J | Xm, X– || X |- ! scope="row" | 5A | || X+, X<sup>(♯5)</sup> |} === Harmonisation par des accords de septième === [[Fichier:Harmonisation gamme do majeur par septiemes chiffre.svg|vignette|upright=2|Harmonisation de la gamme de do majeur par des accords de septième.]] Les accords de septième contiennent une dissonance et créent ainsi une tension. Ils sont très utilisés en jazz. Nous avons représenté ci-contre l'harmonisation de la gamme de ''do'' majeur. La constitution des accords est la suivantes : * tierce majeure (3M) ** quinte juste (5J) *** septième mineure (7m) : sur le degré V, c'est l'accord de septième de dominante V<sup>7</sup><sub>+</sub>, noté X<sup>7</sup> (X pour G), *** septième majeure (7M) : sur les degrés I et IV, appelés « accords de septième majeure » et notés aussi X<sup>maj7</sup> ou X<sup>Δ</sup> (X pour C ou F) ; * tierce mineure (3m) ** quinte juste (5J) *** septième mineure : sur les degrés ii, iii et vi, appelés « accords mineur septième » et notés Xm<sup>7</sup> ou X–<sup>7</sup> (X pour D, E ou A), ** quinte diminuée (5d) *** septième mineure (7m) : sur le degré vii, appelé « accord demi-diminué » (puisque seule la quinte est diminuée) et noté X<sup>∅</sup> ou Xm<sup>7(♭5)</sup> ou X–<sup>7(♭5)</sup> (X pour B) ;<br /> en musique classique, on considère que c'est un accord de neuvième de dominante sans fondamentale. Nous avons donc quatre types d'accords : X<sup>7</sup>, X<sup>maj7</sup>, Xm<sup>7</sup> et X<sup>∅</sup> En jazz, on ajoute souvent la quarte à l'accord de sous-dominante IV (sur le ''fa'' dans une gamme de ''do'' majeur) ; il s'agit ici d'une quarte augmentée (''fa''-''si'') et l'accord est surnommé « accord lydien » mais cette dénomination est erronée (il s'agit d'une mauvaise interprétation de textes antiques). C'est un accord de onzième sans neuvième (la onzième étant l'octave de la quarte), il est noté X<sup>maj7(♯11)</sup> ou X<sup>Δ(♯11)</sup> (ici, F<sup>maj7(♯11)</sup>, ''fa''-''la''-''do''-''mi''-''si'' ou ''fa''-''la''-''si''-''do''-''mi''). {| class="wikitable" |+ Chiffrage jazz des accords de septième |- ! scope="col" rowspan="2" | Tierce ! scope="col" rowspan="2" | Quinte ! scope="col" colspan="2" | Septième |- ! scope="col" | 7m ! scope="col" | 7M |- | rowspan="2" | 3m | 5d || X<sup>∅</sup>, X<sub>m</sub><sup>7(♭5)</sup>, X–<sup>7(♭5)</sup> || |- | rowspan="2" | 5J | X<sub>m</sub><sup>7</sup>, X–<sup>7</sup> || |- | rowspan="2" | 3M | X<sup>7</sup> || X<sup>maj7</sup>, X<sup>Δ</sup> |- | 5A || || X<sub>+</sub><sup>maj7</sup>, X<sub>+</sub><sup>Δ</sup> |} === Modulation et emprunt === Un morceau peut comporter des changements de tonalité; appelés « modulation ». Il y a parfois un court passage dans une tonalité différente, typiquement sur une ou deux mesures, avant de retourner dans la tonalité d'origine : on parle d'emprunt. Lorsqu'il y a une modulation ou un emprunt, les degrés changent. Un même accord peut donc avoir une fonction dans une partie du morceau et une autre fonction ailleurs. L'utilisation d'accord différents, et en particulier d'accord utilisant des altérations accidentelles, indique clairement une modulation. Nous avons vu précédemment que les modulations courantes sont : * les modulations dans les tons voisins ; * les modulations homonymes ; * les marches harmoniques. Une modulation entre une tonalité majeure et mineure change la couleur du passage, * la modulation la plus « douce » est entre les tonalités relatives (par exemple''do'' majeur et ''la'' mineur) car ces tonalités utilisent quasiment les mêmes notes ; * la modulation la plus « voyante » est la modulation homonyme (par exemple entre ''do'' majeur et ''do'' mineur). Une modulation commence souvent sur l'accord de dominante de la nouvelle tonalité. Pour analyser un œuvre, ou pour improviser sur une partie, il est important de reconnaître les modulations. La description de la successind es tonalités s'appelle le « parcours tonal ». === Exercices élémentaires === L'apprentissage des accords passe par quelques exercices élémentaires. '''1. Lire un accord''' Il s'agit de lecture de notes : des notes composant les accords sont écrites « empilées » sur une portée, il faut les lire en énonçant les notes de bas en haut. '''2. Reconnaître la « couleur » d'un accord''' On écoute une triade et il faut dire si c'est une triade majeure ou mineure. Puis, on complexifie l'exercice en ajoutant la septième. '''3. Chiffrage un accord''' Trouver le nom d'un accord à partir des notes qui le composent. '''4. Réalisation d'un accord''' Trouver les notes qui composent un accord à partir de son nom. '''5. Dictée d'accords''' On écoute une succession d'accords et il faut soit écrire les notes sur une portée, soit écrire les noms de accords. [[File:Exercice constitution accord basse chiffree.svg|thumb|Exercice : constitution d'accord à partir de la basse chiffrée.]] '''Exercices de basse chiffrée''' ''Réalisation d'un accord'' Sur la figure suivante, écrire les notes des accords correspondant à la basse chiffrée. Déterminer le degré de la fondamentale pour chaque accord en considérant que nous sommes dans la tonalité de ''sol'' majeur. {{boîte déroulante/début|titre=Solution}} [[File:Exercice constitution accord basse chiffree solution.svg|vignette|Solution.]] # La note de basse est un ''do''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''mi'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''sol''.<br />Le chiffrage « <sup>5</sup> » indique que c'est un accord dans son état fondamental (l'écart entre deux notes consécutives ne dépasse pas la tierce), la fondamentale est donc la basse, ''do'', qui est le degré IV de la tonalité. # La note de basse est un ''si''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''ré'', puis nous appliquons le chiffrage 6 et ajoutons la sixte, ''sol''.<br />Le chiffrage « <sup>6</sup> » indique que c'est un accord dans son premier renversement. En le remettant dans son état fondamental, nous obtenons ''sol-si-ré'', la fondamentale est donc la tonique, le degré I. # La note de basse est un ''la''. Nous ajoutons la tierce (chiffre 3), ''do'', et la sixte (6), ''fa''♯. Nous vérifions que le ''fa''♯ est la sensible (signe +)<br />Nous voyons un « blanc » entre les notes ''do'' et ''fa''♯. En descendant le ''fa''♯ à l'octave inférieure, nous obtenons un empilement de tierces ''fa''♯-''la-do'', le fondamentale est donc ''fa''♯, le degré VII. Nous pouvons le voir comme le deuxième renversement de l'accord de septième de dominante, sans fondamentale. # La note de basse est un ''fa''♯. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''la'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''do'' ; nous vérifions qu'il s'agit bien d'une quinte diminuée (le 5 est barré). Nous appliquons le chiffre 6 et ajoutons la sixte, ''ré''.<br />Nous voyons que les notes ''do'' et ''ré'' sont conjointes (intervalle de seconde). En descendant le ''ré'' à l'octave inférieure, nous obtenons un empilement de tierces ''ré-fa''♯-''la-do'', le fondamentale est donc ''ré'', le degré V. Nous constatons que l'accord chiffré est le premier renversement de l'accord de septième de dominante. {{boîte déroulante/fin}} {{clear}} ''Chiffrage d'accords'' [[Fichier:Exercice chiffrage accord basse chiffree.svg|vignette|Accords à chiffrer.]] Chiffrer les accords ci-contre. {{boîte déroulante/début|titre=Solution}} [[File:Exercice chiffrage accord basse chiffree solution.svg|vignette|Solution.]] # On relève les intervalles en partant de la basse : tierce majeure (3M) et quinte juste (5J). Le chiffrage complet est donc ''fa''<sup>5</sup><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''fa''<sup>5</sup>.<br /> On peut aussi reconnaître que c'est l'accord parfait sur la tonique de la tonalité de ''fa'' majeur dans son état fondamental, le chiffrage d'un accord parfait étant <sup>5</sup>. # On relève les intervalles en partant de la basse : quarte juste (4J), sixte majeure (6M). Le chiffrage complet est donc ''fa''<sup>6</sup><sub>4</sub>.<br /> On peut aussi reconnaître que c'est le second renversement de l'accord ''mi-sol-si'', sur la tonique de la tonalité de ''mi'' mineur, le chiffrage du second renversement d'un accord parfait étant <sup>6</sup><sub>4</sub>. # Les intervalles en partant de la basse sont : tierce mineure (3m), quinte diminuée (5d), sixte mineure (6m). Le chiffrage complet est donc ''mi''<sup>6</sup><small><s>5</s></small><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''mi''<sup>6</sup><sub><s>5</s></sub>.<br /> On reconnaît le premier renversement de l'accord ''do-mi-sol-si''♭, accord de septième de dominante de la tonalité de ''fa'' majeur. # Les intervalles en partant de la basse sont : tierce mineure (3m), quinte juste (5J), septième mineure (7m). Le chiffrage complet est donc ''ré''<sup>7</sup><small>5</small><sub>3</sub> ; c'est typique d'un accord de septième de dominante, son chiffrage est donc ''ré''<sup>7</sup><sub>+</sub>.<br /> On reconnaît l'accord de septième de dominante de la tonalité de ''sol'' mineur dans son état fondamental. {{boîte déroulante/fin}} {{clear}} [[File:Exercice constitution accord notation jazz.svg|thumb|Exercice : constitution d'un accord d'après son chiffrage en notation jazz.]] '''Exercices de notation jazz''' ''Réalisation d'un accord'' Sur la figure suivante, écrire les notes des accords correspondant aux chiffrages. {{boîte déroulante/début|titre=Solution}} [[File:Exercice constitution accord notation jazz solution.svg|thumb|Solution.]] # Il s'agit de la triade majeure de ''do'' dans son état fondamental. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''do-mi-sol''. # Il s'agit de la triade majeure de ''sol''. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''sol-si-ré''. On renverse l'accord afin que la basse soit le ''si'', l'accord est donc ''si-ré-sol''. # Il s'agit de l'accord demi-diminué de ''fa''♯. Les intervalles sont la tierce mineure (3m), la quinte diminuée (5d) et la septième mineure (7m). Les notes sont donc ''fa''♯-''la-do-mi''. Nous renversons l'accord afin que la basse soit le ''la'', l'accord est donc ''a-do-mi-fa''♯. # Il s'agit de l'accord de septième de ''ré''. Les intervalles sont donc la tierce majeure (3M), la quinte juste (5J) et la septième mineure (7m). Les notes sont ''ré-fa''♯''-la-do''. Nous renversons l'accord afin que la basse soit le ''fa''♯, l'accord est donc ''fa''♯''-la-do-ré''. {{boîte déroulante/fin}} {{clear}} ''Chiffrage d'accords'' [[File:Exercice chiffrage accord notation jazz.svg|thumb|Accords à chiffrer.]] Chiffrer les accords ci-contre. {{boîte déroulante/début|titre=Solution}} [[File:Exercice chiffrage accord notation jazz solution.svg|thumb|Solution.]] # Les notes sont toutes sur des interlignes consécutifs, c'est donc un empilement de tierces ; l'accord est dans son état fondamental. Les intervalles sont une tierce majeure (''fa-la'' : 3M) et une quinte juste (''fa-do'' : 5J), c'est donc la triade majeure de ''fa''. Le chiffrage est F. # Il y a un blanc dans l'empilement des notes, c'est donc un accord renversé. En permutant les notes pour n'avoir que des tierces, on trouve l'accord ''mi-sol-si''. Les intervalles sont une tierce mineure (''mi-sol'' : 3m) et une quinte juste (''mi-si'' : 5J), c'est donc la triade mineure de ''mi'' avec un ''si'' à la basse. Le chiffrage est Em/B ou E–/B. # Il y deux notes conjointes, c'est donc un renversement. L'état fondamental de cet accord est ''do-mi-sol-si''♭. Les intervalles sont une tierce majeure (''do-mi'' : 3M), une quinte juste (''do-sol'' : 5J) et une septième mineure (''do-si''♭ : 7m). C'est donc l'accord de ''do'' septième avec un ''mi'' à la basse, chiffré C<sup>7</sup>/E. # Les notes sont toutes sur des interlignes consécutifs, l'accord est dans son état fondamental. Les intervalles sont la tierce mineure (''ré-fa'' : 3m), une quinte juste (''ré-la'' : 5J) et une septième mineure (''ré-do'' : 7m). C'est donc l'accord de ''ré'' mineur septième, chiffré Dm<sup>7</sup> ou D–<sup>7</sup>. {{boîte déroulante/fin}} {{clear}} == Harmonie fonctionnelle == Le choix des accords et de leur succession — la progression des accords — est un élément important d'un morceau, de sa composition. Le compositeur ou la compositrice a bien sûr une liberté totale, mais pour faire des choix, il faut comprendre les conséquences de ces choix, et donc ici, les effets produits par les accords et leur progression. Une des manières d'aborder le sujet est l'harmonie fonctionnelle. === Les trois fonctions des accords === En harmonie tonale, on considère que les accords ont une fonction. Il existe trois fonctions : * la fonction de tonique, {{Times New Roman|I}} ; * la fonction de sous-dominante, {{Times New Roman|IV}} ; * la fonction de dominante, {{Times New Roman|V}}. L'accord de tonique, {{Times New Roman|I}}, est l'accord « stable » de la tonalité par excellence. Il conclut en général les morceaux, et ouvre souvent les morceaux ; il revient fréquemment au cours du morceau. L'accord de dominante, {{Times New Roman|V}}, est un accord qui introduit une instabilité, une tension. En particulier, il contient la sensible (degré {{Times New Roman|VI}}), qui est une note « aspirée » vers la tonique. Cette tension, qui peut être renforcée par l'utilisation d'un accord de septième, est fréquemment résolue par un passage vers l'accord de tonique. Nous avons donc deux mouvements typiques : {{Times New Roman|I}} → {{Times New Roman|V}} (création d'une tension, d'une attente) et {{Times New Roman|V}} → {{Times New Roman|I}} (résolution d'une tension). Les accords de tonique et de dominante ont le cinquième degré en commun, cette note sert donc de pivot entre les deux accords. L'accord de sous-dominante, {{Times New Roman|IV}}, est un accord qui introduit lui aussi une tension, mais moins grande : il ne contient pas la sensible. Notons que s'il est une quarte au-dessus de la tonique, il est aussi une quinte en dessous d'elle ; il est symétrique de l'accord de dominante. Il a donc un rôle similaire à l'accord de dominante, mais atténué. L'accord de sous-dominante aspire soit vers l'accord de dominante, très proche, et l'on a alors une augmentation de la tension ; soit vers l'accord de tonique, un retour vers la stabilité (il a alors un rôle semblable à la dominante). Du fait de ces deux bifurcations possibles — augmentation de la tension ({{Times New Roman|IV}} → {{Times New Roman|V}}) ou retour à la stabilité ({{Times New Roman|IV}} → {{Times New Roman|I}}) —, l'utilisation de l'accord de sous-dominante introduit un certain flottement : si l'on peut facilement prédire l'accord qui suit un accord de dominante, on ne peut pas prédire ce qui suit un accord de sous-dominante. Notons que la composition ne consiste pas à suivre ces règles de manière stricte, ce qui conduirait à des morceaux stéréotypés et plats. Le plaisir d'écoute joue sur une alternance entre satisfaction d'une attente (respect des règles) et surprise (rompre les règles). === Accords remplissant ces fonctions === Les accords sur les autres degrés peuvent se ramener à une de ces trois fonctions : * {{Times New Roman|II}} : fonction de sous-dominante {{Times New Roman|IV}} ; * {{Times New Roman|III}} (très peu utilisé en mode mineur en raison de sa dissonance) et {{Times New Roman|VI}} : fonction de tonique {{Times New Roman|I}} ; * {{Times New Roman|VII}} : fonction de dominante {{Times New Roman|V}}. En effet, les accords étant des empilements de tierces, des accords situés à une tierce l'un de l'autre — {{Times New Roman|I}} ↔ {{Times New Roman|III}}, {{Times New Roman|II}} ↔ {{Times New Roman|IV}}, {{Times New Roman|V}} ↔ {{Times New Roman|VII}}, {{Times New Roman|VI}} ↔ {{Times New Roman|VIII}} ( = {{Times New Roman|I}}) — ont deux notes en commun. On retrouve le fait que l'accord sur le degré {{Times New Roman|VII}} est considéré comme un accord de dominante sans tonique. En mode mineur, l'accord sur le degré {{Times New Roman|III}} est évité, il n'a donc pas de fonction. {|class="wikitable" |+ Fonction des accords |- ! scope="col" | Fondamentale ! scope="col" | Fonction |- | {{Times New Roman|I}} || tonique |- | {{Times New Roman|II}} || sous-dominante faible |- | {{Times New Roman|III}} || tonique faible |- | {{Times New Roman|IV}} || sous-dominante |- | {{Times New Roman|V}} || dominante |- | {{Times New Roman|VI}} || tonique faible |- | {{Times New Roman|VII}} || dominante faible |} Par exemple en ''do'' majeur : * fonction de tonique : '''''do''<sup>5</sup> (C)''', ''mi''<sup>5</sup> (E–), ''la''<sup>5</sup> (A–) ; * fonction de sous-dominante : '''''fa''<sup>5</sup> (F)''', ''ré''<sup>5</sup> (D–) ; * fonction de dominante : '''''sol''<sup>5</sup> (G)''' ou ''sol''<sup>7</sup><sub>+</sub> (G<sup>7</sup>), ''si''<sup> <s>5</s></sup> (B<sup>o</sup>). En ''la'' mineur harmonique : * fonction de tonique : '''''la''<sup>5</sup> (A–)''', ''fa''<sup>5</sup> (F) [, rarement : ''do''<sup>+5</sup> (C<sup>+</sup>)] ; * fonction de sous-dominante : '''''ré''<sup>5</sup> (D–)''', ''si''<sup> <s>5</s></sup> (B<sup>o</sup>) ; * fonction de dominante : '''''mi''<sup>5</sup> (E)''' ou ''mi''<sup>7</sup><sub>+</sub> (E<sup>7</sup>), ''sol''♯<sup> <s>5</s></sup> (G♯<sup>o</sup>). Le fait d'utiliser des accords différents pour remplir une fonction permet d'enrichir l'harmonie, et de jouer sur l'équilibre entre satisfaction d'une attente (on respecte les règles sur les fonctions) et surprise (mais on n'utilise pas l'accord attendu). === Les dominantes secondaires === On utilise aussi des accords de septième dominante se fondant sur un autre degré que la dominante de la gamme ; on parle de « dominante secondaire ». Typiquement, avant un accord de septième de dominante, on utilise parfois un accord de dominante de dominante, dont le degré est alors noté « {{Times New Roman|V}} de {{Times New Roman|V}} » ou « {{Times New Roman|V}}/{{Times New Roman|V}} » ; la fondamentale est de l'accord est alors situé cinq degrés au-dessus de la dominante ({{Times New Roman|V}}), c'est donc le degré {{Times New Roman|IX}}, c'est-à-dire le degré {{Times New Roman|II}} de la tonalité en cours). Ou encore, on utilise un accord de dominante du degré {{Times New Roman|IV}} (« {{Times New Roman|V}} de {{Times New Roman|IV}} », la fondamentale est alors le degré {{Times New Roman|I}}) avant un accord sur le degré {{Times New Roman|IV}} lui-même. Par exemple, en tonalité de ''do'' majeur, on peut trouver un accord ''ré - fa''♯'' - la - do'' (chiffré {{Times New Roman|V}} de {{Times New Roman|V}}<sup>7</sup><sub>+</sub>), avant un accord ''sol - si - ré - fa'' ({{Times New Roman|V}}<sup>7</sup><sub>+</sub>). L'accord ''ré - fa''♯'' - la - do'' est l'accord de septième de dominante des tonalités de ''sol''. Dans la même tonalité, on pourra utiliser un accord ''do - mi - sol - si''♭ ({{Times New Roman|V}} de {{Times New Roman|IV}}<sup>7</sup><sub>+</sub>) avant un accord ''fa - la - do'' ({{Times New Roman|IV}}<sup>5</sup>). Le recours à une dominante secondaire peut atténuer une transition, par exemple avec un enchaînement ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> → ''fa''<sup>5</sup> (C → C<sup>7</sup> → F) qui correspond à un enchaînement {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}} : le passage ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> (C → C<sup>7</sup>) se fait en ajoutant une note (le ''si''♭) et rend naturel le passage ''do'' → ''fa''. Sur les sept degré de la gamme, on ne considère en général que cinq dominantes secondaires : en effet, la dominante du degré {{Times New Roman|I}} est la dominante « naturelle, primaire » de la tonalité (et n'est donc pas secondaire) ; et utiliser la dominante de {{Times New Roman|VII}} consisterait à considérer l'accord de {{Times New Roman|VII}} comme un accord propre, on évite donc les « {{Times New Roman|V}} de “{{Times New Roman|V}}” » (mais les « “{{Times New Roman|V}}” de {{Times New Roman|V}} » sont tout à fait « acceptables »). === Enchaînements classiques === Nous avons donc vu que l'on trouve fréquemment les enchaînements suivants : * pour créer une instabilité : ** {{Times New Roman|I}} → {{Times New Roman|V}}, ** {{Times New Roman|I}} → {{Times New Roman|IV}} (instabilité moins forte mais incertitude sur le sens d'évolution) ; * pour maintenir l'instabilité : ** {{Times New Roman|IV}} → {{Times New Roman|V}} ; * pour résoudre l'instabilité : ** {{Times New Roman|IV}} → {{Times New Roman|I}}, ** {{Times New Roman|V}} → {{Times New Roman|I}}, cas particuliers (voir plus bas) : *** {{Times New Roman|V}}<sup>+4</sup> → {{Times New Roman|I}}<sup>6</sup>, *** {{Times New Roman|I}}<sup>6</sup><sub>4</sub> → {{Times New Roman|V}}<sup>7</sup><sub>+</sub> → {{Times New Roman|I}}<sup>5</sup>. Les degrés indiqués ci-dessus sont les fonctions ; on peut donc utiliser les substitutions suivantes : * {{Times New Roman|I}} par {{Times New Roman|VI}} et, en tonalité majeure, {{Times New Roman|III}} ; * {{Times New Roman|IV}} par {{Times New Roman|II}} ; * {{Times New Roman|V}} par {{Times New Roman|VII}}. Pour enrichir l'harmonie, on peut utiliser les dominantes secondaires, en particulier : * {{Times New Roman|V}} de {{Times New Roman|V}} ({{Times New Roman|II}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|V}}, * {{Times New Roman|V}} de {{Times New Roman|IV}} ({{Times New Roman|I}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|IV}}. On peut enchaîner les enchaînements, par exemple {{Times New Roman|I}} → {{Times New Roman|IV}} → {{Times New Roman|V}}, ou encore {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}}… En jazz, on utilise très fréquemment l'enchaînement {{Times New Roman|II}} → {{Times New Roman|V}} → {{Times New Roman|I}} (deux-cinq-un). On peut bien sûr avoir d'autres enchaînements, mais ces règles permettent d'analyser un grand nombre de morceaux, et donnent des clefs utiles pour la composition. Nous voyons ci-après un certain nombre d'enchaînements courants dans différents styles == Exercice == Un hautboïste travaille la sonate en ''do'' mineur S. 277 de Heinichen. Sur le deuxième mouvement ''Allegro'', il a du mal à travailler un passage en raison des altérations accidentelles. Sur la suggestion de sa professeure, il décide d'analyser la progression d'accords sous-jacente afin que les altérations deviennent logiques. Il s'agit d'un duo hautbois et basson pour lequel les accords ne sont pas chiffrés, le basson étant ici un instrument soliste et non pas un élément de la basse continue. Sur l'extrait suivant, déterminez les basses et la qualité (chiffrage) des accords sous-jacents. Commentez. [[Fichier:Sonate hautbois basson heinichen 2e mvt mes49.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen.]] {{note|L'œuvre est en ''do'' mineur et devrait donc avoir trois bémols à la clef, or ici il n'y en a que deux. En effet, le ''la'' pouvant être bécarre en mode mineur mélodique ascendant, le compositeur a préféré le noter explicitement en altération accidentelle lorsque l'on est en mode mélodique naturel, harmonique ou mélodique descendant. C'est un procédé assez courant à l'époque baroque.}} {{boîte déroulante/début|titre=Solution}} Une des difficultés ici est que les arpèges joués par les instruments sont agrémentés de notes de passage. Les notes de la basse (du basson) sont différentes entre le premier et le deuxième temps de chaque mesure et ne peuvent pas appartenir au même accord. On a donc un accord par temps. Sur le premier temps de chaque mesure, le basson joue une octave. La note concernée est donc la basse de chaque accord. Pour savoir s'il s'agit d'un accord à l'état fondamental ou d'un renversement, on regarde ce que joue le hautbois : dans un mouvement conjoint (succession d'intervalles de secondes), il est difficile de distinguer les notes de l'arpège des notes de passage, mais : les notes des grands intervalles font partie de l'accord. Ainsi, sur le premier temps de la première mesure (la basse est un ''mi''♭), on a une sixte descendante ''sol''-''si''♭ et, à la fin du temps, une tierce descendante ''sol''-''mi''♭. L'accord est donc ''mi''♭-''sol''-''si''♭, c'est un accord de quinte (accord parfait à l'état fondamental). À la fin du premier temps, le basson joue un ''do'', c'est donc une note étrangère. Sur le second temps de la première mesure, le basson joue une tierce ascendante ''fa''-''la''♭, la première note est la basse de l'accord et la seconde une des notes de l'accord. Le hautbois commence par une sixte descendante ''la''♭-''do'', l'accord est donc ''fa''-''la''♭-''do'', un accord de quinte (accord parfait à l'état fondamental). Le ''do'' du basson la fin du premier temps est donc une anticipation. Les autres notes étrangères de la première mesure sont des notes de passage. Mais il faut faire attention : en suivant ce principe, sur les premiers temps des deuxième et troisième mesure, nous aurions des accords de septième d'espèce (puisque la septième est majeure). Or, on ne trouve pas, ou alors exceptionnellement, d'accord de septième d'espèce dans le baroque, mais quasi exclusivement des accords de septième de dominante. Donc au début de la deuxième mesure, le ''la''♮ est une appoggiature du ''si''♭, l'accord est donc ''si''♭-''ré''-''fa'', un asscord de quinte. De même, au début de la troisième mesure, le ''sol'' est une appoggiature du ''la''♭. Il faut donc se méfier d'une analyse purement « mathématique ». Il faut s'attacher à ressentir la musique, et à connaître les styles, pour faire une analyse pertinente. Ci-dessous, nous avons grisé les notes étrangères. [[Fichier:Sonate hautbois basson heinichen 2e mvt mes49 analyse.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen. Analyse de la progression harmonique.]] Le chiffrage jazz équivalent est : : | E♭ F– | B♭<sup>Δ</sup> E♭ | A♭<sup>Δ</sup> D– | G … Nous remarquons une progression assez régulière : : ''mi''♭ ↗[2<sup>de</sup>] ''fa'' | ↘[5<sup>te</sup>] ''si''♭ ↗[4<sup>te</sup>] ''mi''♭ | ↘[5<sup>te</sup>] ''la''♭ ↗[4<sup>te</sup>] ''ré'' | ↘[5<sup>te</sup>] ''sol'' Le ''mi''♭ est le degré {{Times New Roman|III}} de la tonalité principale (''do'' mineur), c'est donc une tonique faible ; il « joue le même rôle » qu'un ''do''. S'il y avait eu un accord de ''do'' au début de l'extrait, on aurait eu une progression parfaitement régulière ↗[4<sup>te</sup>] ↘[5<sup>te</sup>]. Nous avons les modulations suivantes : * mesure 49 : ''do'' mineur naturel (le ''si''♭ n'est pas une sensible) avec un accord sur “{{Times New Roman|I}}” (tonique faible, {{Times New Roman|III}}, pour la première analyse, ou bien tonique forte, {{Times New Roman|I}}, pour la seconde) suivi d'un accord sur {{Times New Roman|IV}} ; * mesure 50 : ''si''♭ majeur avec un accord sur {{Times New Roman|I}} suivi d'un accord sur {{Times New Roman|IV}} ; * mesure 51 : ''la''♭ majeur avec un accord sur {{Times New Roman|I}}, et emprunt à ''do'' majeur avec un accord sur {{Times New Roman|II}} ({{Times New Roman|IV}} faible). On a donc une marche harmonique {{Times New Roman|I}} → {{Times New Roman|IV}} qui descend d'une seconde majeure (un ton) à chaque mesure (''do'' → ''si''♭ → ''la''♭), avec une exception sur la dernière mesure (modulation en cours de mesure et descente d'une seconde mineure au lieu de majeure). Ce passage est donc construit sur une régularité, une règle qui crée un effet d'attente — enchaînement {{Times New Roman|I}}<sup>5</sup> → {{Times New Roman|IV}}<sup>5</sup> avec une marche harmonique d'une seconde majeure descendante —, et des « surprises », des exceptions au début — ce n'est pas un accord {{Times New Roman|I}}<sup>5</sup> mais un accord {{Times New Roman|III}}<sup>5</sup> — et à la fin — modulation en milieu de mesure et dernière descente d'une seconde mineure (½t ''la''♭ → ''sol''). L'extrait ne permet pas de le deviner, mais la mesure 52 est un retour en ''do'' mineur, avec donc une modulation sur la dominante (accord de ''sol''<sup>7</sup><sub>+</sub>, G<sup>7</sup>). {{boîte déroulante/fin}} == Progression d'accords == Comme pour la mélodie, la succession des accords dans un morceau, la progression d'accords, suit des règles. Et comme pour la mélodie, les règles diffèrent d'un style musical à l'autre et la créativité consiste à parfois ne pas suivre ces règles. Et comme pour la mélodie, on part d'un ensemble de notes organisé, d'une gamme caractéristique d'une tonalité, d'un mode. Les accords les plus utilisés pour une tonalité donnée sont les accords dont la fondamentale sont les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la tonalité, en particulier la triade {{Times New Roman|I}}, appelée « accord parfait » ou « accord de tonique », et l'accord de septième {{Times New Roman|V}}, appelé « septième de dominante ». Le fait d'avoir une progression d'accords qui se répète permet de structurer un morceau. Pour les morceaux courts, il participe au plaisir de l'écoute et facilite la mémorisation (par exemple le découpage couplet-refrain d'une chanson). Sur les morceaux longs, une trop grande régularité peut introduire de la lassitude, les longs morceaux sont souvent découpés en parties présentant chacune une progression régulière. Le fait d'avoir une progression régulière permet la pratique de l'improvisation : cadence en musique classique, solo en jazz et blues. ; Note : Le terme « cadence » désigne plusieurs choses différentes, et notamment en harmonie : :* une partie improvisée dans un opéra ou un concerto, sens utilisé ci-dessus ; :* une progression d'accords pour ponctuer un morceau et en particulier pour le conclure, sens utilisé dans la section suivante. === Accords peu utilisés === En mode mineur, l'accord de quinte augmentée {{Times New Roman|III<sup>+5</sup>}} est très peu utilisé. C'est un accord dissonant ; il intervient en général comme appogiature de l'accord de tonique (par exemple en ''la'' mineur : {{Times New Roman|III<sup>+5</sup>}} ''do'' - ''mi'' - ''sol''♯ → {{Times New Roman|I<sup>6</sup>}} ''do'' - ''mi'' - ''la''), ou de l'accord de dominante ({{Times New Roman|III<sup>6</sup><sub>+3</sub>}} ''mi'' - ''sol''♯ - ''do'' → {{Times New Roman|V<sup>5</sup>}} ''mi'' - ''sol''♯ - ''si''). Il peut être aussi utilisé comme préparation à l'accord de sous-dominante (enchaînement {{Times New Roman|III}} → {{Times New Roman|IV}}). Par ailleurs, il a une constitution symétrique — c'est l'empilement de deux tierces majeures — et ses renversements ont les mêmes intervalles à l'enharmonie près (quinte augmentée/sixte mineure, tierce majeure/quarte diminuée). De ce fait, un même accord est commun, par renversement et à l'enharmonie près, à trois tonalités : le premier renversement de l'accord ''do'' - ''mi'' - ''sol''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur) est enharmonique à ''mi'' - ''sol''♯ - ''si''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''do''♯ mineur) ; le second renversement est enharmonique à ''la''♭ - ''do'' - ''mi'' ({{Times New Roman|III}}<sup>e</sup> degré de ''fa'' mineur). === Accords très utilisés === Les trois accords les plus utilisés sont les accords de tonique (degré {{Times New Roman|I}}), de sous-dominante ({{Times New Roman|IV}}) et de dominante ({{Times New Roman|V}}). Ils interviennent en particulier en fin de phrase, dans les cadences. L'accord de dominante sert souvent à introduire une modulation : la modulation commence sur l'accord de dominante de la nouvelle tonalité. On note que l'accord de sous-dominante est situé une quinte juste en dessous de la tonique, les accords de dominante et de sous-dominante sont donc symétriques. En jazz, on utilise également très fréquemment l'accord de la sus-tonique (degré {{Times New Roman|II}}), souvent dans des progressions {{Times New Roman|II}} - {{Times New Roman|V}} (- {{Times New Roman|I}}). Rappelons que l'accord de sus-tonique a la fonction de sous-dominante. === Cadences et ''turnaround'' === Le terme « cadence » provient de l'italien ''cadenza'' et désigne la « chute », la fin d'un morceau ou d'une phrase musicale. On distingue deux types de cadences : * les cadences conclusive, qui créent une sensation de complétude ; * les cadences suspensives, qui crèent une sensation d'attente. ==== Cadence parfaite ==== [[Fichier:Au clair de le lune cadence parfaite.midi|thumb|''Au clair de la lune'', harmonisé avec une cadence parfaite (italienne).]] [[Fichier:Au clair de le lune mineur cadence parfaite.midi|thumb|''Idem'' mais en mode mineur harmonique.]] La cadence parfaite est l'enchaînement de l'accord de dominante suivi de l'accord parfait : {{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}}, les deux accord étant à l'état fondamental. Elle donne une impression de stabilité et est donc très souvent utilisée pour conclure un morceau. C'est une cadence conclusive. On peut aussi utiliser l'accord de septième de dominante, la dissonance introduisant une tension résolue par l'accord parfait : {{Times New Roman|V<sup>7</sup><sub>+</sub> - I<sup>5</sup>}}. Elle est souvent précédée de l'accord construit sur le IV<sup>e</sup> degré, appelé « accord de préparation », pour former la cadence italienne : {{Times New Roman|IV<sup>5</sup> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}}. Elle est également souvent précédée du second renversement de l'accord de tonique, qui est alors appelé « appoggiature de la cadence » : {{Times New Roman|I<sup>6</sup><sub>4</sub> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}} (on remarque que les accords {{Times New Roman|I}}<sup>6</sup><sub>4</sub> et {{Times New Roman|V}}<sup>5</sup> ont la basse en commun, et que l'on peut passer de l'un à l'autre par un mouvement conjoint sur les autres notes). {{clear}} ==== Demi-cadence ==== [[Fichier:Au clair de le lune demi cadence.midi|thumb|''Au clair de la lune'', harmonisé avec une demi-cadence.]] Une demi-cadence est une phrase ou un morceau se concluant sur l'accord construit sur le cinquième degré. Il provoque une sensation d'attente, de suspens. Il s'agit en général d'une succession {{Times New Roman|II - V}} ou {{Times New Roman|IV - V}}. C'est une cadence suspensive. On utilise rarement un accord de septième de dominante. {{clear}} ==== Cadence rompue ou évitée ==== La cadence rompue, ou cadence évitée, est succession d'un accord de dominante et d'un accord de sus-dominante, {{Times New Roman|V}} - {{Times New Roman|VI}}. C'est une cadence suspensive. ==== Cadence imparfaite ==== Une cadence imparfaite est une cadence {{Times New Roman|V - I}}, comme la cadence parfaite, mais dont au moins un des deux accords est dans un état renversé. ==== Cadence plagale ==== La cadence plagale — du grec ''plagios'', oblique, en biais — est la succession de l'accord construit sur le quatrième degré, suivi de l'accord parfait : {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}. Elle peut être utilisée après une cadence parfaite ({{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}} - {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}). Elle donne un caractère solennel, voire religieux — elle est parfois appelée « cadence amen » —, elle a un côté antique qui rappelle la musique modale et médiévale<ref>{{lien web |url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/la-cadence-amen-ou-comment-se-dire-adieu-7191921 |titre=La cadence « Amen » ou comment se dire adieu |auteur=Max Dozolme (MAXXI Classique) |site=France Musique |date=2025-04-25 |consulté le=2025-04-25}}.</ref>. C'est une cadence conclusive. ==== {{lang|en|Turnaround}} ==== [[Fichier:Au clair de le lune turnaround.midi|thumb|Au clair de la lune, harmonisé en style jazz : accords de 7{{e}}, anatole suivie d'un ''{{lang|en|turnaround}}'' ii-V-I.]] Le terme ''{{lang|en|turnaround}}'' signifie revirement, retournement. C'est une succession d'accords que fait la transition entre deux parties, en créant une tension-résolution. Le ''{{lang|en|turnaround}}'' le plus courant est la succession {{Times New Roman|II - V - I}}. On utilise également fréquemment l'anatole : {{Times New Roman|I - VI - II - V}}. {| class="wikitable" |+ Progressions typiques d'accords dans une tonalité majeure ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Cadence<br />parfaite<br /> {{Times New Roman|V - I}} ! scope="col" | Cadence<br />italienne<br />{{Times New Roman|IV - V - I}} ! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou IV - V}} ! scope="col" | Cadence<br />plagale<br />{{Times New Roman|IV - I}} ! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}} ! scope="col" | Anatole<br />{{Times New Roman|I - vi - ii - V}} |- |''Do'' majeur || || G - C || F - G - C || Dm - G ou F - G || F - C || Dm - G - C || C - Am - Dm - G |- |''Sol'' majeur || ''fa''♯ || D - G || C - D - G || Am - D ou C - D || C - G || Am - D - G || G - Em - Am - D |- |''Ré'' majeur || ''fa''♯, ''do''♯ || A - D || G - A - D || Em - A ou G - A || G - D || Em - A - D || D - Bm - Em - A |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || E - A || D - E - A || Bm - E ou D - E || D - A || Bm - E - A || A - F♯m - B - E |- | ''Fa'' majeur || ''si''♭ || C - F || B♭ - C - F || Gm - C ou B♭ - C || B♭ - F || Gm - C - F || F - Dm - Gm - C |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || F - B♭ || E♭ - F - B♭ || Cm - F ou E♭ - F || E♭ - B♭ || Cm - F - B♭ || B♭ - Gm - Cm - F |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || B♭ - E♭ || A♭ - B♭ - E♭ || Fm - B♭ ou A♭ - B♭ || A♭ - E♭ || Fm - B♭ - E♭ || Gm - Cm - Fm - B♭ |} {| class="wikitable" |+ Progressions typiques d'accords dans une tonalité mineure ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Cadence<br />parfaite<br />{{Times New Roman|V - i}} ! scope="col" | Cadence<br />italienne<br />{{Times New Roman|iv - V - i}} ! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou iv - V}} ! scope="col" | Cadence<br />plagale<br />{{Times New Roman|iv - i}} ! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}} ! scope="col" | Anatole<br />{{Times New Roman|i - VI - ii - V}} |- | ''La'' mineur<br />harmonique || || E - Am || Dm - E - Am || B° - E ou Dm - E || Dm - Am || B° - E - Am || Am - F - B° - E |- | ''Mi'' mineur<br />harmonique || ''fa''♯ || B - Em || Am - B - Em || F♯° - B ou Am - B || Am - Em || F♯° - B - Em || Em - C - F♯° - B |- | ''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || F♯ - Bm || Em - F♯ - Bm || C♯° - F♯ ou Em - F♯ || Em - Bm || C♯° - F♯ - Bm || Bm - G - C♯° - F♯ |- | ''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || C♯ - F♯m || Bm - C♯ - F♯m || G♯° - C♯ ou Bm - C♯ || Bm - F♯m || G♯° - C♯ - F♯m || A+ - D - G♯° - C♯ |- | ''Ré'' mineur<br />harmonique || ''si''♭ || A - Dm || Gm - A - Dm || E° - A ou Gm - A || Gm - Dm || E° - A - Dm || Dm - B♭ - E° - A |- | ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || D - Gm || Cm - D - Gm || A° - D ou Cm - D || Cm - Gm|| A° - D - Gm || Gm - E♭ - A° - D |- | ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || G - Cm || Fm - G - Cm || D° - G ou Fm - G || Fm - Dm || D° - G - Cm || Cm - A♭ - D° - G |} ==== Exemple : ''La Mer'' ==== : {{lien web | url = https://www.youtube.com/watch?v=PXQh9jTwwoA | titre = Charles Trenet - La mer (Officiel) [Live Version] | site = YouTube | auteur = Charles Trenet | consulté le = 2020-12-24 }} Le début de ''La Mer'' (Charles Trenet, 1946) est en ''do'' majeur et est harmonisé par l'anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (C - Am - Dm - G<sup>7</sup>) sur deux mesures, jouée deux fois ({{Times New Roman|1=<nowiki>|I-vi|ii-V</nowiki><sup>7</sup><nowiki>|</nowiki>}} × 2). Viennent des variations avec les progressions {{Times New Roman|I-III-vi-V<sup>7</sup>}} (C - E - Am - G<sup>7</sup>) puis la « progression ’50s » (voir plus bas) {{Times New Roman|I-vi-IV-VI<sup>7</sup>}} (C - Am - F - A<sup>7</sup>, on remarque que {{Times New Roman|IV}}/F est le relatif majeur du {{Times New Roman|ii}}/Dm de l'anatole), jouées chacune une fois sur deux mesure ; puis cette première partie se conclut par une demie cadence {{Times New Roman|ii-V<sup>7</sup>}} sur une mesure puis une dernière anatole sur trois mesures ({{Times New Roman|1=<nowiki>|I-vi|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}}). Cela constitue une première partie « A » sur douze mesures qui se termine par une demi-cadence ({{Times New Roman|ii-V<sup>7</sup>}}) qui appelle une suite. Cette partie A est jouée une deuxième fois mais la fin est modifiée pour la transition : les deux dernières mesures {{Times New Roman|<nowiki>|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}} deviennent {{Times New Roman|<nowiki>|ii-V</nowiki><sup>7</sup><nowiki>|I|</nowiki>}} (|Dm-G7|C|), cette partie « A’ » se conclut donc par une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}). Le morceau passe ensuite en tonalité de ''mi'' majeur, donc une tierce au dessus de ''do'' majeur, sur six mesures. Cette partie utilise une progression ’50s {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (E - C♯m - A - B<sup>7</sup>), qui est rappelons-le une variation de l'anatole, l'accord {{Times New Roman|ii}} (Fm) étant remplacé par son relatif majeur {{Times New Roman|IV}} (A). Cette anatole modifiée est jouée deux fois puis la partie en ''mi'' majeur se conclut par l'accord parfait {{Times New Roman|I}} joué sur deux mesures (|E|E|), on a donc, avec la mesure précédente, avec une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}). Suivent ensuite six mesures en ''sol'' majeur, donc à nouveau une tierce au dessus de ''mi'' majeur. Elle comporte une progression {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (G - Em - C - D<sup>7</sup>), donc anatole avec substitution du {{Times New Roman|ii}}/Am par son relatif majeur {{Times New Roman|VI}}/C (progression ’50s), puis une anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (G - Em - Am - D<sup>7</sup>) et deux mesure sur la tonique {{Times New Roman|I-I<sup>7</sup>}} (G - G<sup>7</sup>), formant à nouveau une cadence parfaite. La fin sur un accord de septième, dissonant, appelle une suite. Cette partie « B » de douze mesures comporte donc deux parties similaires « B1 » et « B2 » qui forment une marche harmonique (montée d'une tierce). Le morceau se conclut par une reprise de la partie « A’ » et se termine donc par une cadence parfaite. Nous avons une structure A-A’-B-A’ sur 48 mesures, proche la forme AABA étudiée plus loin. Donc ''La Mer'' est un morceau structuré autour de l'anatole avec des variations (progression ’50s, substitution du {{Times New Roman|ii}} par son relatif majeur {{Times New Roman|IV}}) et comportant une marche harmonique dans sa troisième partie. Les parties se concluent par des ''{{lang|en|turnarounds}}'' sous la forme d'une cadence parfaite ou, pour la partie A, par une demi-cadence. {| border="1" rules="rows" frame="hsides" |+ Structure de ''La Mer'' |- align="center" | | colspan="12" | ''do'' majeur | |- align="center" ! scope="row" rowspan=2 | A | colspan="2" | anatole | colspan="2" | // | colspan="2" | variation | colspan="2" | ’50s | ½ c. | colspan="3" | anatole | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii}} || <nowiki>|</nowiki> {{Times New Roman|V<sup>7</sup>}} || <nowiki>|</nowiki> |- align="center" ! scope="row" rowspan="2" | A’ | colspan="2" | anatole | colspan="2" | // | colspan="2" | variation | colspan="2" | ’50s | ½ c. | colspan="2" | anatole | c.p. | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> |- align="center" | | colspan="6" | B1 : ''mi'' majeur | colspan="6" background="lightgray" | B2 : ''sol'' majeur | |- align="center" ! scope="row" rowspan="2" | B | colspan="2" | ’50s | colspan="2" | // |colspan="2" | c.p. | colspan="2" | ’50s | colspan="2" | // |colspan="2" | c.p. | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I<sup>7</sup>}} || <nowiki>|</nowiki> |- ! scope="row" | A’ | colspan="12" | | |} === Progression blues === La musique blues est apparue dans les années 1860. Elle est en général bâtie sur une grille d'accords ''({{lang|en|changes}})'' immuable de douze mesures ''({{lang|en|twelve-bar blues}})''. C'est sur cet accompagnement qui se répète que s'ajoute la mélodie — chant et solo. Cette structure est typique du blues et se retrouve dans ses dérivés comme le rock 'n' roll. Le rythme est toujours un rythme ternaire syncopé ''({{lang|en|shuffle, swing, groove}}, ''notes inégales'')'' : la mesure est à quatre temps, mais la noire est divisée en noire-croche en triolet, ou encore triolet de croche en appuyant la première et la troisième. La mélodie se construit en général sur une gamme blues de six degrés (gamme pentatonique mineure avec une quarte augmentée), mais bien que la gamme soit mineure, l'harmonie est construite sur la gamme majeure homonyme : un blues en ''fa'' a une mélodie sur la gamme de ''fa'' mineur, mais une harmonie sur la gamme de ''fa'' majeur. La grille d'accord comporte les accords construits sur les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la gamme majeure homonyme. Les accords sont souvent des accords de septième (donc avec une tierce majeure et une septième mineure), il ne s'agit donc pas d'une harmonisation de gamme diatonique (puisque la septième est majeure sur l'accord de tonique). Par exemple, pour un blues en ''do'' : * accord parfait de do majeur, C ({{Times New Roman|I}}<sup>er</sup> degré) ; * accord parfait de fa majeur, F ({{Times New Roman|IV}}<sup>e</sup> degré) ; * accord parfait de sol majeur, G ({{Times New Roman|V}}<sup>e</sup> degré). Il existe quelques morceaux harmonisés avec des accords mineurs, comme par exemple ''As the Years Go Passing By'' d'Albert King (Duje Records, 1959). La progression blues est organisée en trois blocs de quatre mesures ayant les fonctions suivantes (voir ci-dessus ''[[#Harmonie fonctionnelle|Harmonie fonctionnelle]]'') : * quatre mesures toniques ; * quatre mesures sous-dominantes ; * quatre mesures dominantes. La forme la plus simple, que Jeff Gardner appelle « forme A », est la suivante : {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, forme A |- ! scope="row" | Tonique | width="50px" | I | width="50px" | I | width="50px" | I | width="50px" | I |- ! scope="row" | Sous-domminante | width="50px" | IV | width="50px" | IV | width="50px" | I | width="50px" | I |- ! scope="row" | Dominante | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | V |} La progression {{Times New Roman|I-V}} des deux dernières mesures forment le ''{{lang|en|turnaround}}'', la demie cadence qui lance le cycle suivant. Nous présentons ci-dessous un exemple typique de ligne de basse ''({{lang|en|walking bass}})'' pour le ''{{lang|en|turnaround}}'' d'un blues en ''la'' : [[Fichier:Turnaround classique blues en la.svg|Exemple typique de ligne de basse pour un ''turnaround'' de blues en ''la''.]] [[Fichier:Blues mi harmonie elementaire.midi|thumb|Blues en ''mi'', harmonisé de manière élémentaire avec une ''{{lang|en|walking bass}}''.]] Vous pouvez écouter ci-contre une harmonisation typique d'un blues en ''mi''. Les accords sont exécutés par une basse marchante ''({{lang|en|walking bass}})'', qui joue une arpège sur la triade avec l'ajout d'une sixte majeure et d'une septième mineure, et par une guitare qui joue un accord de puissance ''({{lang|en|power chord}})'', qui n'est composé que de la fondamentale et de la quinte juste, avec une sixte en appoggiature. La forme B s'obtient en changeant la deuxième mesure : on joue un degré {{Times New Roman|IV}} au lieu d'un degré {{Times New Roman|I}}. La progression {{Times New Roman|I-IV}} sur les deux premières mesures est appelé ''{{lang|en|quick change}}''. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, forme B |- | width="50px" | I | width="50px" | IV | width="50px" | I | width="50px" | I |- | width="50px" | IV | width="50px" | IV | width="50px" | I | width="50px" | I |- | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | V |} Par exemple, ''Sweet Home Chicago'' (Robert Johnson, 1936) est un blues en ''fa'' ; sa grille d'accords, aux variations près, suit une forme B : {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression de ''Sweet Home Chicago'' |- | width="50px" | F | width="50px" | B♭ | width="50px" | F | width="50px" | F |- | width="50px" | B♭ | width="50px" | B♭ | width="50px" | F | width="50px" | F |- | width="50px" | C7 | width="50px" | B♭7 | width="50px" | F7 | width="50px" | C7 |} : Écouter {{lien web | url =https://www.youtube.com/watch?v=dkftesK2dck | titre = Robert Johnson "Sweet Home Chicago" | auteur = Michal Angel | site = YouTube | date = 2007-12-09 | consulté le = 2020-12-17 }} Les formes C et D s'obtiennent à partir des formes A et B en changeant le dernier accord par un accord sur le degré {{Times New Roman|I}}, ce qui forme une cadence plagale. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, formes C et D |- | colspan="4" | … |- | colspan="4" | … |- | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | I |} L'harmonie peut être enrichie, notamment en jazz. Voici par exemple une grille du blues souvent utilisés en bebop. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Exemple de progression de blues bebop sur une base de forme B |- | width="60px" | I<sup>7</sup> | width="60px" | IV<sup>7</sup> | width="60px" | I<sup>7</sup> | width="60px" | V–<sup>7</sup> <nowiki>|</nowiki> I<sup>7</sup> |- | width="60px" | IV<sup>7</sup> | width="60px" | IV<sup>7</sup> | width="60px" | I<sup>7</sup> | width="60px" | VI<sup>7 ♯9 ♭13</sup> |- | width="60px" | II–<sup>7</sup> | width="60px" | V<sup>7</sup> | width="60px" | V<sup>7</sup> <nowiki>|</nowiki> IV<sup>7</sup> | width="60px" | II–<sup>7</sup> <nowiki>|</nowiki> V<sup>7</sup> |} On peut aussi trouver des blues sur huit mesures, sur seize mesures comme ''Watermelon Man'' de Herbie Hancock (album ''Takin' Off'', Blue Note, 1962) ou ''Let's Dance'' de Jim Lee (interprété par Chris Montez, Monogram, 1962) * {{lien web |url= https://www.dailymotion.com/video/x5iduwo |titre=Herbie Hancock - Watermelon Man (1962) |auteur=theUnforgettablesTv |site=Dailymotion |date=2003 |consulté le=2021-02-09 }} * {{lien web |url=https://www.youtube.com/watch?v=6JXshurYONc |titre=Let's Dance |auteur=Chris Montez |site=YouTube |date=2016-08-06 |consulté le=2021-02-09 }} À l'inverse, certains blues peuvent avoir une structure plus simple que les douze mesure ; par exemple ''Hoochie Coochie Man'' de Willie Dixon (interprété par Muddy Waters sous le titre ''Mannish Boy'', Chicago Blues, 1954) est construit sur un seul accord répété tout le long de la chanson. * {{lien web |url=https://www.dailymotion.com/video/x5iduwo |titre=Muddy Waters - Hoochie Coochie Man |auteur=Muddy Waters |site=Dailymotion |date=2012 | consulté le=2021-02-09 }} === Cadence andalouse === La cadence andalouse est une progression de quatre accords, descendant par mouvement conjoint : * en mode de ''mi'' (mode phrygien) : {{Times New Roman|IV}} - {{Times New Roman|III}} - {{Times New Roman|II}} - {{Times New Roman|I}} ;<br />par exemple en ''mi'' phrygien : Am - G - F - E ; en ''do'' phrygien : Fm - E♭ - D♭ - C ;<br />on notera que le degré {{Times New Roman|III}} est diésé dans l'accord final (ou bécarre s'il est bémol dans la tonalité) ; * en mode mineur : {{Times New Roman|I}} - {{Times New Roman|VII}} - {{Times New Roman|VI}} - {{Times New Roman|V}} ;<br />par exemple en ''la'' mineur : Am - G - F - E ; en ''do'' mineur : Cm - B♭ - A♭ - m ;<br />comme précédemment, on notera que le degré {{Times New Roman|VII}} est diésé dans l'accord final. === Progressions selon le cercle des quintes === [[Fichier:Cercle quintes degres tonalite majeure.svg|vignette|Cercle des quinte justes (parcouru dans le sens des aiguilles d'une montre) des degrés d'une tonalité majeure.]] La progression {{Times New Roman|V-I}} est la cadence parfaite, mais on peut aussi l'employer au milieu d'un morceau. Cette progression étant courte, sa répétition crée de la lassitude ; on peut la compléter par d'autres accords séparés d'une quinte juste, en suivant le « cercle des quintes » : {{Times New Roman|I-V-IX}}, la neuvième étant enharmonique de la seconde, on obtient {{Times New Roman|I-V-II}} ; en retournant la progression, on obtient le turnaround {{Times New Roman|II-V-I}}. En jazz, dans le morceau ''Topsy'' (Edgar Battle et Eddir Durham), la partie <span style="border-color: black; border-style: solid; border-width: thin;">C</span> est composée d'une progression en quintes descendantes d'accords de septième de dominante : D<sup>7</sup> - G<sup>7</sup> - C<sup>7</sup> - F<sup>7</sup> - B♭<sup>7</sup> ; les improvisatiosn sur cette partie peuvent donc se faire en modulant en ''sol'' (majeur ou mineur) - ''do'' (M/m) - ''fa'' (M/m) - ''si''♭ (M/m) - ''mi''♭ (M/m). On peut continuer de décrire le cercle des quintes : {{Times New Roman|I-V-II-VI}}, on obtient l'anatole dans le désordre ; on peut à l'inverse étendre les quintes vers la gauche, {{Times New Roman|IV-I-V-II-VI}}. En musique populaire, on trouve fréquemment une progression fondée sur les accord {{Times New Roman|I}}, {{Times New Roman|IV}}, {{Times New Roman|V}} et {{Times New Roman|VI}}, popularisée dans les années 1950. La « progression années 1950 », « progression ''{{lang|en|fifties ('50)}}'' » ''({{lang|en|'50s progression}})'' est dans l'ordre {{Times New Roman|I-VI-IV-V}}. On trouve aussi cette progression en musique classique. Si la tonalité est majeure, la triade sur la sus-dominante est mineure, les autres sont majeures, on notera donc souvent {{Times New Roman|I-vi-IV-V}}. On peut avoir des permutations circulaires (le dernier accord venant au début, ou vice-versa) : {{Times New Roman|vi-IV-V-I}}, {{Times New Roman|IV-V-I-vi}} et {{Times New Roman|V-I-vi-IV}}. {| class="wikitable" |+ Accords selon la tonalité ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" style="font-family:Times New Roman" | I ! scope="col" style="font-family:Times New Roman" | IV ! scope="col" style="font-family:Times New Roman" | V ! scope="col" style="font-family:Times New Roman" | vi |- |''Do'' majeur || || C || F || G || Am |- |''Sol'' majeur || ''fa''♯ || G || C || D || Em |- |''Ré'' majeur || ''fa''♯, ''do''♯ || D || G || A || Bm |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A || D || E || F♯m |- | ''Fa'' majeur || ''si''♭ || F || B♭ || C || Dm |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭ || E♭ || F || Gm |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭ || A♭ || B♭ || Cm |} Par exemple, en tonalité de ''do'' majeur, la progression {{Times New Roman|I-vi-IV-V}} sera C-Am-F-G. Il existe d'autres progressions utilisant ces accords mais dans un autre ordre, typiquement {{Times New Roman|I–IV–vi–V}} ou une de ses permutations circulaires : {{Times New Roman|IV–vi–V-I}}, {{Times New Roman|vi–V-I-IV}} ou {{Times New Roman|V-I-IV-vi}}. Ou dans un autre ordre. PV Nova l'illustre dans plusieurs de ses « expériences » dans la version {{Times New Roman|vi-V-IV-I}}, soit Am-G-F-C, ou encore {{Times New Roman|vi-IV-I-V}}, soit Am-F-C-G : : {{lien web | url = https://www.youtube.com/watch?v=w08LeZGbXq4 | titre = Expérience n<sup>o</sup> 6 — La Happy Pop | auteur = PV Nova | site = YouTube | date = 2011-08-20 | consulté le = 2020-12-13 }} et cela devient un gag récurrent avec son « chapeau des accords magiques qu'on nous ressort à toutes les sauces » : {{lien web | url = https://www.youtube.com/watch?v=VMY_vc4nZAU | titre = Expérience n<sup>o</sup> 14 — La Soupe dou Brasil | auteur = PV Nova | site = YouTube | date = 2012-10-03 | consulté le = 2020-12-17 }} Cette récurrence est également parodiée par le groupe The Axis of Awesome avec ses « chansons à quatre accords » ''({{lang|en|four-chords song}})'', dans une sketch où ils mêlent 47 chansons en utilisant l'ordre {{Times New Roman|I-V-vi-IV}} : : {{lien web | url = https://www.youtube.com/watch?v=oOlDewpCfZQ | titre = 4 Chords | Music Videos | The Axis Of Awesome | auteur = The Axis of Awesome | site = YouTube | date = 2011-07-20 | consulté le = 2020-12-17 }} {{boîte déroulante/début|titre=Chansons mêlées dans le sketch}} # Journey : ''Don't Stop Believing'' ; # James Blunt : ''You're Beautiful'' ; # Black Eyed Peas : ''Where Is the Love'' ; # Alphaville : ''Forever Young'' ; # Jason Mraz : ''I'm Yours'' ; # Train : ''Hey Soul Sister'' ; # The Calling : ''Wherever You Will Go'' ; # Elton John : ''Can You Feel The Love Tonight'' (''Le Roi lion'') ; # Akon : ''Don't Matter'' ; # John Denver : ''Take Me Home, Country Roads'' ; # Lady Gaga : ''Paparazzi'' ; # U2 : ''With Or Without You'' ; # The Last Goodnight : ''Pictures of You'' ; # Maroon Five : ''She Will Be Loved'' ; # The Beatles : ''Let It Be'' ; # Bob Marley : ''No Woman No Cry'' ; # Marcy Playground : ''Sex and Candy'' ; # Men At Work : ''Land Down Under'' ; # thème de ''America's Funniest Home Videos'' (équivalent des émissions ''Vidéo Gag'' et ''Drôle de vidéo'') ; # Jack Johnson : ''Taylor'' ; # Spice Girls : ''Two Become One'' ; # A Ha : ''Take On Me'' ; # Green Day : ''When I Come Around'' ; # Eagle Eye Cherry : ''Save Tonight'' ; # Toto : ''Africa'' ; # Beyonce : ''If I Were A Boy'' ; # Kelly Clarkson : ''Behind These Hazel Eyes'' ; # Jason DeRulo : ''In My Head'' ; # The Smashing Pumpkins : ''Bullet With Butterfly Wings'' ; # Joan Osborne : ''One Of Us'' ; # Avril Lavigne : ''Complicated'' ; # The Offspring : ''Self Esteem'' ; # The Offspring : ''You're Gonna Go Far Kid'' ; # Akon : ''Beautiful'' ; # Timberland featuring OneRepublic : ''Apologize'' ; # Eminem featuring Rihanna : ''Love the Way You Lie'' ; # Bon Jovi : ''It's My Life'' ; # Lady Gaga : ''Pokerface'' ; # Aqua : ''Barbie Girl'' ; # Red Hot Chili Peppers : ''Otherside'' ; # The Gregory Brothers : ''Double Rainbow'' ; # MGMT : ''Kids'' ; # Andrea Bocelli : ''Time To Say Goodbye'' ; # Robert Burns : ''Auld Lang Syne'' ; # Five for fighting : ''Superman'' ; # The Axis of Awesome : ''Birdplane'' ; # Missy Higgins : ''Scar''. {{boîte déroulante/fin}} Vous pouvez par exemple jouer les accords C-G-Am-F ({{Times New Roman|I-V-vi-IV}}) et chanter dessus ''{{lang|en|Let It Be}}'' (Paul McCartney, The Beattles, 1970) ou ''Libérée, délivrée'' (Robert Lopez, ''La Reine des neiges'', 2013). La progression {{Times New Roman|I-V-vi-IV}} est considérée comme « optimiste » tandis que sa variante {{Times New Roman|iv-IV-I-V}} est considérée comme « pessimiste ». On peut voir la progression {{Times New Roman|I-vi-IV-V}} comme une variante de l'anatole {{Times New Roman|I-vi-ii-V}}, obtenue en remplaçant l'accord de sustonique {{Times New Roman|ii}} par l'accord de sous-dominante {{Times New Roman|IV}} (son relatif majeur, et degré ayant la même fonction). ==== Exemples de progression selon le cercle des quintes en musique classique ==== [[Fichier:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.midi|vignette|Dietrich Buxtehude, Psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]] Cette progression selon la cercle des quintes, sous la forme {{Times New Roman|I-vi-IV-V}}, apparaît déjà au {{pc|xvii}}<sup>e</sup> siècle dans le psaume 42 ''Quem ad modum desiderat cervis'' (BuxVW92) de Dietrich Buxtehude (1637-1707). Le morceau est en ''fa'' majeur, la progression d'accords est donc F-Dm-B♭-C. : {{lien web | url = https://www.youtube.com/watch?v=8FmV9l1RqSg | titre = D. Buxtehude - Quemadmodum desiderat cervus, BuxWV 92 | auteur = Longobardo | site = YouTube | date = 2013-04-06 | consulté la = 2021-01-01 }} [[File:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.svg|vignette|450x450px|center|Dietrich Buxtehude, psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]] {{clear}} [[Fichier:JSBach BWV140 cantate 4 mesures.midi|vignette|J.-S. Bach, cantate BWV140, quatre premières mesures.]] On la trouve également dans l'ouverture de la cantate ''{{lang|de|Wachet auf, ruft uns die Stimme}}'' de Jean-Sébastien Bach (BWV140, 1731). Le morceau est en ''mi''♭ majeur, la progression d'accords est donc E♭-Cm-A♭<sup>6</sup>-B♭. [[Fichier:JSBach BWV140 cantate 4 mesures.svg|vignette|center|J.-S. Bach, cantate BWV140, quatre premières mesures.|alt=|517x517px]] {{clear}} [[Fichier:Mozart K310 Sonate8 mesures 45 a 49.midi|vignette|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]] La même progression est utilisée par Mozart, par exemple dans le premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778), la progression d'accords est C-Am-F-G qui correspond à la progression {{Times New Roman|III-i-VI-VII}} de ''la'' mineur, mais à la progression {{Times New Roman|I-vi-IV-V}} de la gamme relative, ''do'' majeur . [[Fichier:Mozart K310 Sonate8 mesures 45 a 49.svg|vignette|center|500px|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]] === Substitution tritonique === Un des accords les plus utilisés est donc l'accord de septième de dominante, {{Times New Roman|V<sup>7</sup><sub>+</sub>}} qui contient les degrés {{Times New Roman|V}}, {{Times New Roman|VII}}, {{Times New Roman|II}} ({{Times New Roman|IX}}) et {{Times New Roman|IV}}({{Times New Roman|XI}}) ; par exemple, en tonalité de ''do'' majeur, l'accord de ''sol'' septième (G<sup>7</sup>) contient les notes ''sol''-''si''-''ré''-''fa''. Si l'on prend l'accord dont la fondamentale est trois tons (triton) au-dessus ou en dessous — l'octave contenant six tons, on arrive sur la même note —, {{Times New Roman|♭II<sup>7</sup>}}, ici ''ré''♭ septième (D♭<sup>7</sup>), celui-ci contient les notes ''ré''♭-''fa''-''la''♭-''do''♭, cette dernière note étant l'enharmonique de ''si''. Les deux accords G<sup>7</sup> et D♭<sup>7</sup> ont donc deux notes en commun : le ''fa'' et le ''si''/''do''♭. Il est donc fréquent en jazz de substituer l'accord {{Times New Roman|V<sup>7</sup><sub>+</sub>}} par l'accord {{Times New Roman|♭II<sup>7</sup>}}. Par exemple, la progression {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|V<sup>7</sup>}}-{{Times New Roman|I<sup>&Delta;</sup>}} devient {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|♭II<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}}. C'est un procédé courant de réharmonisation (le fait de remplacer un accord par un autre dans un morceau existant). Les six substitutions possibles sont donc : C<sup>7</sup>↔F♯<sup>7</sup> - D♭<sup>7</sup>↔G<sup>7</sup> - D<sup>7</sup>↔A♭<sup>7</sup> - E♭<sup>7</sup>↔A<sup>7</sup> - E<sup>7</sup>↔B♭<sup>7</sup> - F<sup>7</sup>↔B<sup>7</sup>. [[Fichier:Übermäsiger Terzquartakkord.jpg|vignette|Exemple de cadence parfaite en ''do'' majeur avec substitution tritonique (sixte française).]] Dans l'accord D♭<sup>7</sup>, si l'on remplace le ''do''♭ par son ''si'' enharmonique, on obtient un accord de sixte augmentée : ''ré''♭-''fa''-''la''♭-''si''. Cet accord est utilisé en musique classique depuis la Renaissance ; on distingue en fait trois accords de sixte augmentée : * sixte française ''ré''♭-''fa''-''sol''-''si'' ; * sixte allemande : ''ré''♭-''fa''-''la''♭-''si'' ; * sixte italienne : ''ré''♭-''fa''-''si''. Par exemple, le ''Quintuor en ''ut'' majeur'' de Franz Schubert (1828) se termine par une cadence parfaite dont l'accord de dominante est remplacé par une sixte française ''ré''♭-''fa''-''si''-''sol''-''si'' (''ré''♭ aux violoncelles, ''fa'' à l'alto, ''si''-''sol'' aux seconds violons et ''si'' au premier violon). [[Fichier:Schubert C major Quintet ending.wav|vignette|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]] [[Fichier:Schubert C major Quintet ending.png|vignette|center|upright=2.5|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]] === Autres accords de substitution === Substituer un accord consiste à utiliser un accord provenant d'une tonalité étrangère à la tonalité en cours. À la différence d'une modulation, la substitution est très courte et ne donne pas l'impression de changer de tonalité ; on a juste un sentiment « étrange » passager. Un court passage dans une autre tonalité est également appelée « emprunt ». Nous avons déjà vu plusieurs méthodes de substitution : * utilisation d'une note étrangère : une note étrangère — note de passage, appoggiature, anticipation, retard… — crée momentanément un accord hors tonalité ; en musique classique, ceci n'est pas considéré comme un accord en propre, mais en jazz, on parle « d'accord de passage » et « d'accord suspendu » ; * utilisation d'une dominante secondaire : l'accord de dominante secondaire est hors tonalité ; le but ici est de faire une cadence parfaite, mais sur un autre degré que la tonique de la tonalité en cours ; * la substitution tritonique, vue ci-dessus, pour remplacer un accord de septième de dominante. Une dernière méthode consiste à remplacer un accord par un accord d'une gamme de même tonique, mais d'un autre mode ; on « emprunte » ''({{lang|en|borrow}})'' l'accord d'un autre mode. Par exemple, substituer un accord de la tonalité de ''do'' majeur par un accord de la tonalité de ''do'' mineur ou de ''do'' mode de ''mi'' (phrygien). Donc en ''do'' majeur, on peut remplacer un accord de ''ré'' mineur septième (D<sub>m</sub><sup>7</sup>) par un accord de ''ré'' demi-diminué (D<sup>⌀</sup>, D<sub>m</sub><sup>7&flat;5</sup>) qui est un accord appartenant à la donalité de ''la'' mineur harmonique. === Forme AABA === La forme AABA est composée de deux progressions de huit mesures, notées A et B ; cela représente trente-deux mesures au total, on parle donc souvent en anglais de la ''{{lang|en|32-bars form}}''. C'est une forme que l'on retrouve dans de nombreuses chanson de comédies musicales de Broadway comme ''Have You Met Miss Jones'' (''{{lang|en|I'd Rather Be Right}}'', 1937), ''{{lang|en|Over the Rainbow}}'' (''Le Magicien d'Oz'', Harold Harlen, 1939), ''{{lang|en|All the Things You Are}}'' (''{{lang|en|Very Warm for may}}'', 1939). Par exemple, la version de ''{{lang|en|Over the Rainbow}}'' chantée par Judy Garland est en ''la''♭ majeur et la progression d'accords est globalement : * A (couplet) : A♭-Fm | Cm-A♭ | D♭ | Cm-A♭ | D♭ | D♭-F | B♭-E♭ | A♭ * B (pont) : A♭ | B♭m | Cm | D♭ | A♭ | B♭-G | Cm-G | B♭m-E♭ soit en degrés : * A : {{Times New Roman|<nowiki>I-vi | iii-I | IV | iii-IV | IV | IV-vi | II-V | I</nowiki>}} * B : {{Times New Roman|<nowiki>I | ii | iii | IV | I | II-VII | iii-VII | ii-V</nowiki>}} Par rapport aux paroles de la chanson, on a * A : couplet 1 ''« {{lang|en|Somewhere […] lullaby}} »'' ; * A : couplet 2 ''« {{lang|en|Somewhere […] really do come true}} »'' ; * B : pont ''« {{lang|en|Someday […] you'll find me}} »'' ; * A : couplet 3 ''« {{lang|en|Somewhere […] oh why can't I?}} »'' ; : {{lien web | url = https://www.youtube.com/watch?v=1HRa4X07jdE | titre = Judy Garland - Over The Rainbow (Subtitles) | site = YouTube | auteur = Overtherainbow | consulté le = 2020-12-17 }} Une mise en œuvre de la forme AABA couramment utilisée en jazz est la forme anatole (à le pas confondre avec la succession d'accords du même nom), en anglais ''{{lang|en|rythm changes}}'' car elle s'inspire du morceau ''{{lang|en|I Got the Rythm}}'' de George Gerschwin (''Girl Crazy'', 1930) : * A : {{Times New Roman|I–vi–ii–V}} (succession d'accords « anatole ») ; * B : {{Times New Roman|III<sup>7</sup>–VI<sup>7</sup>–II<sup>7</sup>–V<sup>7</sup>}} (les fondamentales forment une succession de quartes, donc parcourent le « cercle des quintes » à l'envers). Par exemple, ''I Got the Rythm'' étant en ''ré''♭ majeur, la forme est : * A : D♭ - B♭m - E♭m - A♭ * B : F7 - B♭7 - E♭7 - A♭7 === Exemples === ==== Début du Largo de la symphonie du Nouveau Monde ==== [[File:Largo nouveau monde 5 1res mesures.svg|vignette|Partition avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]] [[File:Largo nouveau monde 5 1res mesures.midi|vignette|Fichier son avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]] Nous avons reproduit ci-contre les cinq premières mesure du deuxième mouvement Largo de la symphonie « Du Nouveau Monde » (symphonie n<sup>o</sup> 9 d'Antonín Dvořák, 1893). Cliquez sur l'image pour l'agrandir. Vous pouvez écouter cette partie jouée par un orchestre symphonique : * {{lien web |url =https://www.youtube.com/watch?v=y2Nw9r-F_yQ?t=565 |titre = Dvorak Symphony No.9 "From the New World" Karajan 1966 |site=YouTube (Seokjin Yoon) |consulté le=2020-12-11 }} (à 9 min 25), par le Berliner Philharmoniker, dirigé par Herbert von Karajan (1966) ; * {{lien web |url = https://www.youtube.com/watch?v=ASlch7R1Zvo |titre=Dvořák: Symphony №9, "From The New World" - II - Largo |site=YouTube (diesillamusicae) |consulté le=2020-12-11 }} : Wiener Philharmoniker, dirigé par Herbert von Karajan (1985). {{clear}} Cette partie fait intervenir onze instruments monodiques (ne jouant qu'une note à la fois) : des vents (trois bois, sept cuivres) et une percussion. Certains de ces instruments sont transpositeurs (les notes sur la partition ne sont pas les notes entendues). Jouées ensemble, ces onze lignes mélodiques forment des accords. Pour étudier cette partition, nous réécrivons les parties des instruments transpositeurs en ''do'' et les parties en clef d’''ut'' en clef de ''fa''. Nous regroupons les parties en clef de ''fa'' d'un côté et les parties en clef de ''sol'' d'un autre. {{boîte déroulante|Résultat|contenu=[[File:Largo nouveau monde 5 1res mesures transpositeurs en do.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde, en do.]]}} Nous pouvons alors tout regrouper sous la forme d'un système de deux portées clef de ''fa'' et clef de ''sol'', comme une partition de piano. {{boîte déroulante|Résultat|contenu= [[Fichier:Largo nouveau monde 5 1res mesures accords.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords.]] }} [[Fichier:Largo nouveau monde 5 1res mesures accords.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords.]] {{clear}} Ensuite, nous ne gardons que la basse et les notes médium. Nous changeons éventuellement certaines notes d'octave afin de n'avoir que des superpositions de tierce ou de quinte (état fondamental des accords, en faisant ressortir les notes manquantes). {{boîte déroulante|Résultat|contenu= [[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords simplifiés.]] }} [[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords simplifiés.]] Vous pouvez écouter cette partie jouée par un quintuor de cuivres (trompette, bugle, cor, trombone, tuba), donc avec des accords de cinq notes : : {{lien web |url=https://www.youtube.com/watch?v=pWfe60nbvjA |titre = Largo from The New World Symphony by Dvorak |site=YouTube (The Chamberlain Brass) |consulté le=2020-12-11 }} : The American Academy of Arts & Letters in New York City (2017). Nous allons maintenant chiffrer les accords. Pour établir la basse chiffrée, il nous faut déterminer le parcours harmonique. Pour le premier accord, les tonalités les plus simples avec un ''sol'' dièse sont ''la'' majeur et ''fa'' dièse mineur ; comme le ''mi'' est bécarre, nous retenons ''la'' majeur, il s'agit donc d'un accord de quinte sur la dominante (les accords de dominante étant très utilisés, cela nous conforte dans notre choix). Puis nous avons un ''si'' bémol, nous pouvons être en ''fa'' majeur ou en ''ré'' mineur ; nous retenons ''fa'' majeur, c'est donc le renversement d'un accord sur le degré {{Times New Roman|II}}. Dans la deuxième mesure, nous revenons en ''la'' majeur, puis, avec un ''la'' et un ''ré'' bémols, nous sommes en ''la'' bémol majeur ; nous avons donc un accord de neuvième incomplet sur la sensible, ou un accord de onzième incomplet sur la dominante. Dans la troisième mesure, nous passons en ''ré'' majeur, avec un accord de dominante. Puis, nous arrivons dans la tonalité principale, avec le renversement d'un accord de dominante sans tierce suivi d'un accord de tonique. Nous avons donc une cadence parfaite, conclusion logique d'une phrase. La progression des accords est donc : {| class="wikitable" ! scope="row" | Tonalité | ''la'' M - ''fa'' M || ''la'' M - ''la''♭ M || ''ré'' M - ''ré''♭ M || ''ré''♭ M |- ! scope="row" | Accords | {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|II}}<sup>6</sup><sub>4</sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|“V”}}<sup>9</sup><sub><s>5</s></sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|V}}<sup>+4</sup> || {{Times New Roman|I}}<sup>5</sup> |} Dans le chiffrage jazz, nous avons donc : * une triade de ''mi'' majeur, E ; * une triade de ''sol'' majeur avec un ''ré'' en basse : G/D ; * à nouveau un E ; * un accord de ''sol'' neuvième diminué incomplet, avec un ''ré'' bémol en basse : G dim<sup>9</sup>/D♭ ; * un accord de ''la'' majeur, A ; * un accord de ''la'' bémol septième avec une ''sol'' bémol à la basse : A♭<sup>7</sup>/G♭ ; * la partie se conclue par un accord parfait de ''ré''♭ majeur, D♭. Soit une progression E - G/D | E - G dim<sup>9</sup>/D♭ | A - A♭<sup>7</sup>/G♭ | D♭. [[Fichier:Largo nouveau monde 5 1res mesures accords chiffres.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde en accords simplifiés.]] {{clear}} ==== Thème de Smoke on the Water ==== Le morceau ''Smoke on the Water'' du groupe Deep Purple (album ''Machine Head'', 1972) possède un célèbre thème, un riff ''({{lang|en|rythmic figure}})'', joué à la guitare sous forme d'accords de puissance ''({{lang|en|power chords}})'', c'est-à-dire des accords sans tierce. Le morceau est en tonalité de ''sol'' mineur naturel (donc avec un ''fa''♮) avec ajout de la note bleue (''{{lang|en|blue note}}'', quinte diminuée, ''ré''♭), et les accords composant le thème sont G<sup>5</sup>, B♭<sup>5</sup>, C<sup>5</sup> et D♭<sup>5</sup>, ce dernier accord étant l'accord sur la note bleue et pouvant être considéré comme une appoggiature (indiqué entre parenthèse ci-après). On a donc ''a priori'', sur les deux premières mesures, une progression {{Times New Roman|I-III-IV}} puis {{Times New Roman|I-III-(♭V)-IV}}. Durant la majeure partie du thème, la guitare basse tient la note ''sol'' en pédale. {{note|En jazz, la qualité « <sup>5</sup> » indique que l'on n'a que la quinte (et donc pas la tierce), contrairement à la notation de basse chiffrée.}} : {{lien web | url = https://www.dailymotion.com/video/x5ili04 | titre = Deep Purple — Smoke on the Water (Live at Montreux 2006) | auteur = Deep Purple | site = Dailymotion | date = 2016 | consulté le = 2020-12-31 }} Cependant, cette progression forme une mélodie, on peut donc plus la voir comme un contrepoint, la superposition de deux voies ayant un mouvement conjoint, joué par un seul instrument, la guitare, la voie 2 étant jouée une quarte juste en dessous de la voie 1 (la quarte juste descendante étant le renversement de la quinte juste ascendante) : * voie 1 (aigu) : | ''sol'' - ''si''♭ - ''do'' | ''sol'' - ''si''♭ - (''ré''♭) - ''do'' | ; * voie 2 (grave) : | ''ré'' - ''fa'' - ''sol'' | ''ré'' - ''fa'' - (''la''♭) - ''sol'' |. En se basant sur la basse (''sol'' en pédale), nous pouvons considérer que ces deux mesures sont accompagnées d'un accord de Gm<sup>7</sup> (''sol''-''si''♭-''ré''-''fa''), chaque accord de la mélodie comprenant à chaque fois au moins une note de cet accord à l'exception de l'appogiature. {| class="wikitable" |+ Mise en évidence des notes de l'accord Gm<sup>7</sup> |- ! scope="row" | Accords | G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> |- ! scope="row" | Voie 1 | '''''sol''''' || '''''si''♭''' || ''do'' |- ! scope="row" | Voie 2 | '''''ré''''' || '''''fa''''' || '''''sol''''' |- ! scope="row" | Basse | '''''sol''''' || '''''sol''''' || '''''sol''''' |} Sur les deux mesures suivantes, la basse varie et suit les accords de la guitare avec un retard sur le dernier accord : {| class="wikitable" |+ Voies sur les mesure 3-4 du thème |- ! scope="row" | Accords | G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> || B♭<sup>5</sup> || G<sup>5</sup> |- ! scope="row" | Voie 1 | ''sol'' || ''si''♭ || ''do'' || ''si''♭ || ''sol'' |- ! scope="row" | Voie 2 | ''ré'' || ''fa'' || ''sol'' || ''fa'' || ''ré'' |- ! scope="row" | Basse | ''sol'' || ''sol'' || ''do'' || ''si''♭ || ''si''♭-''sol'' |} Le couplet de cette chanson est aussi organisé sur une progression de quatre mesures, la guitare faisant des arpèges sur les accords G<sup>5</sup> (''sol''-''ré''-''sol'') et F<sup>5</sup> (''fa''-''do''-''fa'') : : | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-F<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | soit une progression {{Times New Roman|<nowiki>| I-I | I-I | I-VII | I-I |</nowiki>}}. Nous pouvons aussi harmoniser le riff du thème sur cette progression, avec un accord F (''fa''-''la''-''do'') ; nous pouvons aussi nous rappeler que l'accord sur le degré {{Times New Roman|VII}} est plus volontiers considéré comme un accord de septième de dominante {{Times New Roman|V<sup>7</sup>}}, soit ici un accord Dm<sup>7</sup> (''ré''-''fa''-''la''-''do''). On peut donc considérer la progression harmonique sur le thème : : | Gm-Gm | Gm-Gm | Gm-F ou Dm<sup>7</sup> | Gm-Gm |. Cette analyse permet de proposer une harmonisation enrichie du morceau, tout en se rappelant qu'une des forces du morceau initial est justement la simplicité de sa structure, qui fait ressortir la virtuosité des musiciens. Nous pouvons ainsi comparer la version album à la version concert avec orchestre ou à la version latino de Pat Boone. À l'inverse, le groupe Psychostrip, dans une version grunge, a remplacé les accords par une ligne mélodique : * le thème ne contient plus qu'une seule voie (la guitare ne joue pas des accords de puissance) ; * dans les mesures 9 et 10, la deuxième guitare joue en contrepoint de type mouvement inverse, qui est en fait la voie 2 jouée en miroir ; * l'arpège sur le couplet est remplacé par une ligne mélodique en ostinato sur une gamme blues. {| class="wikitable" |+ Contrepoint sur les mesures 9 et 10 |- ! scope="row" | Guitare 1 | ''sol'' ↗ ''si''♭ ↗ ''do'' |- ! scope="row" | Guitare 2 | ''sol'' ↘ ''fa'' ↘ ''ré'' |} * {{lien web | url = https://www.dailymotion.com/video/x5ik234 | titre = Deep Purple — Smoke on the Water (In Concert with the London Symphony Orchestra, 1999) | auteur = Deep Purple | site = Dailymotion | date = 2016 | consulté le = 2020-12-31 }} * {{lien web | url = https://www.youtube.com/watch?v=MtUuNzVROIg | titre = Pat Boone — Smoke on the Water (In a Metal Mood, No More Mr. Nice Guy, 1997) | auteur = Orrore a 33 Giri | site = YouTube | date = 2019-06-24 | consulté le = 2020-12-31 }} * {{lien web | url = https://www.youtube.com/watch?v=n7zLlZ8B0Bk | titre = Smoke on the Water (Heroes, 1993) | auteur = Psychostrip | site = YouTube | date = 2018-06-20 | consulté le = 2020-12-31 }} == Accords et improvisation == Nous avons vu précédemment (chapitre ''[[../Gammes et intervalles#Modes et improvisation|Gammes et intervalles &gt; Modes et improvisation]]'') que le choix d'un mode adapté permet d'improviser sur un accord. L'harmonisation des gammes permet, en inversant le processus, d'étendre notre palette : il suffit de repérer l'accord sur une harmonisaiton de gamme, et d'utiliser cette gamme-là, dans le mode correspondant du degré de l'accord (voir ci-dessus ''[[#Harmonisation par des accords de septième|Harmonisation par des accords de septième]]''). Par exemple, nous avons vu que l'accord sur le septième degré d'une gamme majeure était un accord demi-diminué ; nous savons donc que sur un accord demi-diminué, nous pouvons improviser sur le mode correspondant au septième degré, soit le mode de ''si'' (locrien). Un accord de septième de dominante étant commun aux deux tonalités homonymes (par exemple ''fa'' majeur et ''fa'' mineur pour un ''do''<sup>7</sup><sub>+</sub> / C<sup>7</sup>), nous pouvons utiliser le mode de ''sol'' de la gamme majeure (mixolydien) ou de la gamme mineure mineure (mode phrygien dominant, ou phrygien espagnol) pour improviser. Mais l'accord de septième de dominante est aussi l'accord au début d'une grille blues ; on peut donc improviser avec une gamme blues, même si la tierce est majeure dans l'accord et mineure dans la gamme. [[Fichier:Mode improvisation accords do complet.svg]] == Autres accords courants == [[fichier:Cluster cdefg.png|vignette|Agrégat ''do - ré - mi - fa - sol''.]] Nous avons vu précédemment l'harmonisation des tonalités majeures et mineures harmoniques par des triades et des accords de septième ; certains accords étant rarement utilisés (l'accord sur le degré {{Times New Roman|III}} et, pour les tonalités mineures harmoniques, l'accord sur la tonique), certains accords étant utilisés comme des accords sur un autre degré (les accords sur la sensible étant considérés comme des accords de dominante sans fondamentale). Dans l'absolu, on peut utiliser n'importe quelle combinaison de notes, jusqu'aux agrégats, ou ''{{lang|en|clusters}}'' (mot anglais signifiant « amas », « grappe ») : un ensemble de notes contigües, séparées par des intervalles de seconde. Dans la pratique, on reste souvent sur des accords composés de superpositions de tierces, sauf dans le cas de transitions (voir la section ''[[#Notes étrangères|Notes étrangère]]''). === En musique classique === On utilise parfois des accords dont les notes ne sont pas dans la tonalité (hors modulation). Il peut s'agir d'accords de passage, de notes étrangères, par exemple utilisant un chromatisme (mouvement conjoint par demi-tons). Outre les accords de passage, les autres accords que l'on rencontre couramment en musique classique sont les accords de neuvième, et les accords de onzième et treizième sur tonique. Ces accords sont simplement obtenus en continuant à empiler les tierces. Il n'y a pas d'accord d'ordre supérieur car la quinzième est deux octaves au-dessus de la fondamentale. Comme pour les accords de septième, on distingue les accords de neuvième de dominante et les accords de neuvième d'espèce. Dans le cas de la neuvième de dominante, il y a une différence entre les tonalités majeures et mineures : l'intervalle de neuvième est respectivement majeur et mineur. Les chiffrages des renversements peuvent donc différer. Comme pour les accords de septième de dominante, on considère que les accords de septième sur le degré {{Times New Roman|VI}} sont en fait des accords de neuvième de dominante sans fondamentale. Les accords de neuvième d'espèce sont en général préparés et résolus. Préparés : la neuvième étant une note dissonante (c'est à une octave près la seconde de la fondamentale), l'accord qui précède doit contenir cette note, mais dans un accord consonant ; la neuvième est donc commune avec l'accord précédent. Résolus : la dissonance est résolue en abaissant la neuvième par un mouvement conjoint. Par exemple, en tonalité de ''do'' majeur, si l'on veut utiliser un accord de neuvième d'espèce sur la tonique ''(do - mi - sol - si - ré)'', on peut utiliser avant un accord de dominante ''(sol - si - ré)'' en préparation puis un accord parfait sur le degré {{Times New Roman|IV}} ''(fa - la - do)'' en résolution ; nous avons donc sur la voie la plus aigüe la succession ''ré'' (consonant) - ''ré'' (dissonant) - ''do'' (consonant). On rencontre également parfois des accords de onzième et de treizième. On omet en général la tierce, car elle est dissonante avec la onzième. L'accord le plus fréquemment rencontré est l'accord sur la tonique : on considère alors que c'est un accord sur la dominante que l'on a enrichi « par le bas », en ajoutant une quinte inférieure. par exemple, dans la tonalité de ''do'' majeur, l'accord ''do - sol - si - ré - fa'' est considéré comme un accord de septième de dominante sur tonique, le degré étant noté « {{Times New Roman|V}}/{{Times New Roman|I}} ». De même pour l'accord ''do - sol - si - ré - fa - la'' qui est considéré comme un accord de neuvième de dominante sur tonique. === En jazz === En jazz, on utilise fréquemment l'accord de sixte à la place de l'accord de septième majeure sur la tonique. Par exemple, en ''do'' majeur, on utilise l'accord C<sup>6</sup> ''(do - mi - sol - la)'' à la place de C<sup>Δ</sup> ''(do - mi - sol - si)''. On peut noter que C<sup>6</sup> est un renversement de Am<sup>7</sup> et pourrait donc se noter Am<sup>7</sup>/C ; cependant, le fait de le noter C<sup>6</sup> indique que l'on a bien un accord sur la tonique qui s'inscrit dans la tonalité de ''do'' majeur (et non, par exemple, de ''la'' mineur naturelle) — par rapport à l'harmonie fonctionnelle, on remarquera que Am<sup>7</sup> a une fonction tonique, l'utilisation d'un renversement de Am<sup>7</sup> à la place d'un accord de C<sup>Δ</sup> est donc logique. Les accords de neuvième, onzième et treizième sont utilisés comme accords de septième enrichis. Le chiffrage suit les règles habituelles : on ajoute un « 9 », un « 11 » ou un « 13 » au chiffrage de l'accord de septième. On utilise également des accords dits « suspendus » : ce sont des accords de transition qui sont obtenus en prenant une triade majeure ou mineure et en remplaçant la tierce par la quarte juste (cas le plus fréquent) ou la seconde majeure. Plus particulièrement, lorsque l'on parle simplement « d'accord suspendu » sans plus de précision, cela désigne l'accord de neuvième avec une quarte suspendue, noté « 9sus4 » ou simplement « sus ». == L'harmonie tonale == L'harmonie tonale est un ensemble de règle assez strictes qui s'appliquent dans la musique savante européenne, de la période baroque à la période classique classique ({{pc|xiv}}<sup>e</sup>-{{pc|xviii}}<sup>e</sup> siècle). Certaines règles sont encore largement appliquées dans divers styles musicaux actuels, y compris populaire (rock, rap…), d'autres sont au contraire ignorées (par exemple, un enchaînement de plusieurs accords de même qualité forme un mouvement parallèle, ce qui est proscrit en harmonie tonale). De nos jours, on peut voir ces règles comme des règles « de bon goût », et leur application stricte comme une manière de composer « à la manière de ». Précédemment, nous avons vu la progression des accords. Ci-après, nous abordons aussi la manière dont les notes de l'accord sont réparties entre plusieurs voix, et comment on construit chaque voix. === Concepts fondamentaux === ; Consonance : Les intervalles sont considérés comme « plus ou moins consonants » : :* consonance parfaite : unisson, quinte et octave ; :* consonance mixte (parfaite dans certains contextes, imparfaite dans d'autres) : quarte ; :* consonance imparfaite : tierce et sixte ; :* dissonance : seconde et septième. ; Degrés : Certains degrés sont considérés comme « forts », « meilleurs », ce sont les « notes tonales » : {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (sous-dominante) et {{Times New Roman|V}} (dominante). [[Fichier:Mouvements harmoniques.svg|vignette|upright=0.75|Mouvements harmoniques.]] ; Mouvements : Le mouvement décrit la manière dont les voix évoluent les unes par rapport aux autres : :# Mouvement parallèle : les voix sont séparées par un intervalle constant. :# Mouvement oblique : une voix reste constante, c'est le bourdon ; l'autre monte ou descend. :# Mouvement contraire : une voix descend, l'autre monte. :# Échange de voix : les voix échangent de note ; les mélodies se croisent mais on a toujours le même intervalle harmonique. {{clear}} === Premières règles === ; Règle du plus court chemin : Quand on passe d'un accord à l'autre, la répartition des notes se fait de sorte que chaque voix fait le plus petit mouvement possible. Notamment : si les deux accords ont des notes en commun, alors les voix concernées gardent la même note. : Les deux voix les plus importantes sont la voix aigüe — soprano — et la voix la plus grave — basse. Ces deux voix sont relativement libres : la voix de soprano a la mélodie, la voix de basse fonde l'harmonie. La règle du plus court chemin s'applique surtout aux voix intermédiaires ; si l'on a des mouvements conjoints, ou du moins de petits intervalles — c'est le sens de la règle du plus court chemin —, alors les voix sont plus faciles à interpréter. Cette règle évite également que les voix n'empiètent l'une sur l'autre (voir la règle « éviter le croisement des voix »). ; Éviter les consonances parfaites consécutives :* Lorsque deux voix sont à l'unisson ou à l'octave, elles ne doivent pas garder le même intervalle, l'effet serait trop plat. :* Lorsque deux voix sont à la quarte ou à la quinte, elles ne doivent pas garder le même intervalle, car l'effet est trop dur. : Pour éviter cela, lorsque l'on part d'un intervalle juste, on a intérêt à pratiquer un mouvement contraire aux voix qui ne gardent pas la même note, ou au moins un mouvement direct : les voix vont dans le même sens, mais l'intervalle change. : Notez que même avec le mouvement contraire, on peut avoir des consonances parfaites consécutives, par exemple si une voix fait ''do'' aigu ↗ ''sol'' aigu et l'autre ''sol'' médium ↘ ''do'' grave. : L'interdiction des consonances parfaites consécutives n'a pas été toujours appliquée, le mouvement parallèle strict a d'ailleurs été le premier procédé utilisé dans la musique religieuse au {{pc|x}}<sup>e</sup> siècle. On peut par exemple utiliser des quintes parallèles pour donner un style médiéval au morceau. On peut également utiliser des octaves parallèles sur plusieurs notes afin de créer un effet de renforcement de la mélodie. : Par ailleurs, les consonances parfaites consécutives sont acceptées lorsqu'il s'agit d'une cadence (transition entre deux parties ou bien conclusion du morceau). ; Éviter le croisement des voix : Les voix sont organisées de la plus grave à la plus aigüe. Deux voix n'étant pas à l'unisson, celle qui est plus aigüe ne doit pas devenir la plus grave et ''vice versa''. ; Soigner la partie soprano : Comme c'est celle qu'on entend le mieux, c'est en général celle qui porte la mélodie principale. On lui applique des règles spécifiques : :# Si elle chante la sensible dans un accord de dominante ({{Times New Roman|V}}), alors elle doit monter à la tonique, c'est-à-dire que la note suivante sera la tonique située un demi-ton au dessus. :# Si l'on arrive à une quinte ou une octave entre les parties basse et soprano par un mouvement direct, alors sur la partie soprano, le mouvement doit être conjoint. On doit donc arriver à cette situation par des notes voisines au soprano. ; Préférer certains accords : Les deux degrés les plus importants sont la tonique ({{Times New Roman|I}}) et la dominante ({{Times New Roman|V}}), les accords correspondants ont donc une importance particulière. : À l'inverse, l'accord de sensible ({{Times New Roman|VII}}) n'est pas considéré comme ayant une fonction harmonique forte. On le considère comme un accord de dominante affaibli. En tonalité mineure, on évite également l'accord de médiante ({{Times New Roman|III}}). : Donc on utilise en priorité les accords de : :# {{Times New Roman|I}} et {{Times New Roman|V}}. :# Puis {{Times New Roman|II}}, {{Times New Roman|IV}}, {{Times New Roman|VI}} ; et {{Times New Roman|III}} en mode majeur. :# On évite {{Times New Roman|VII}} ; et {{Times New Roman|III}} en mode mineur. ; Préférer certains enchaînements : Les enchaînements d'accord peuvent être classés par ordre de préférence. Par ordre de préférence décroissante (du « meilleur » au « moins bon ») : :# Meilleurs enchaînements : quarte ascendante ou descendante. Notons que la quarte est le renversement de la quinte, on a donc des enchaînements stables et naturels, mais avec un intervalle plus court qu'un enchaînement de quintes. :# Bons enchaînements : tierce ascendante ou descendante. Les accords consécutifs ont deux notes en commun. :# Enchaînements médiocres : seconde ascendante ou descendante. Les accords sont voisins, mais ils n'ont aucune note en commun. On les utilise de préférence en mouvement ascendant, et on utilise surtout les enchaînements {{Times New Roman|IV}}-{{Times New Roman|V}}, {{Times New Roman|V}}-{{Times New Roman|VI}} et éventuellement {{Times New Roman|I}}-{{Times New Roman|II}}. :# Les autres enchaînements sont à éviter. : On peut atténuer l'effet d'un enchaînement médiocre en plaçant le second accord sur un temps faible ou bien en passant par un accord intermédiaire. [[Fichier:Progression Vplus4 I6.svg|thumb|Résolution d'un accord de triton (quarte sensible) vers l'accord de sixte de la tonique.]] ; La septième descend par mouvement conjoint : Dans un accord de septième de dominante, la septième — qui est donc le degré {{Times New Roman|IV}} — descend par mouvement conjoint — elle est donc suivie du degré {{Times New Roman|III}}. : Corolaire : un accord {{Times New Roman|V}}<sup>+4</sup> se résout par un accord {{Times New Roman|I}}<sup>6</sup> : on a bien un enchaînement {{Times New Roman|V}} → {{Times New Roman|I}}, et la 7{{e}} (degré {{Times New Roman|IV}}), qui est la basse de l'accord {{Times New Roman|V}}<sup>+4</sup>, descend d'un degré pour donner la basse de l'accord {{Times New Roman|I}}<sup>6</sup> (degré {{Times New Roman|III}}). {{clear}} [[Fichier:Progression I64 V7plus I5.svg|thumb|Accord de sixte et de quarte cadentiel.]] ; Un accord de sixte et quarte est un accord de passage : Le second renversement d'un accord parfait est soit une appoggiature, soit un accord de passage, soit un accord de broderie. : S'il s'agit de l'accord de tonique {{Times New Roman|I}}<sup>6</sup><sub>4</sub>, c'est « accord de sixte et quarte de cadence », l'appoggiature de l'accord de dominante de la cadence parfaite. {{clear}} Mais il faut appliquer ces règles avec discernement. Par exemple, la voix la plus aigüe est celle qui s'entend le mieux, c'est donc elle qui porte la mélodie principale. Il est important qu'elle reste la plus aigüe. La voix la plus grave porte l'harmonie, elle pose les accords, il est donc également important qu'elle reste la plus grave. Ceci a deux conséquences : # Ces deux voix extrêmes peuvent avoir des intervalles mélodiques importants et donc déroger à la règle du plus court chemin : la voix aigüe parce que la mélodie prime, la voix de basse parce que la progression d'accords prime. # Les croisements des voix intermédiaires sont moins critiques. Par ailleurs, si l'on applique strictement toutes les règles « meilleurs accords, meilleurs enchaînements », on produit un effet conventionnel, stéréotypé. Il est donc important d'utiliser les solutions « moins bonnes », « médiocres » pour apporter de la variété. Ajoutons que les renversements d'accords permettent d'avoir plus de souplesse : on reste sur le même accord, mais on enrichit la mélodie sur chaque voix. Le ''Bolero'' de Maurice Ravel (1928) brise un certain nombre de ces règles. Par exemple, de la mesure 39 à la mesure 59, la harpe joue des secondes. De la mesure 149 à la mesure 165, les piccolo jouent à la sixte, dans des mouvement strictement parallèle, ce qui donne d'ailleurs une sonorité étrange. À partir de la mesure 239, de nombreux instruments jouent en mouvement parallèles (piccolos, flûtes, hautbois, cor, clarinettes et violons). === Application === [[Fichier:Harmonisation possible de frere jacques exercice.svg|vignette|Exercice : harmoniser ''Frère Jacques''.]] Harmoniser ''Frère Jacques''. Nous considérons un morceau à quatre voix : basse, ténor, alto et soprano. La soprano chante la mélodie de ''Frère Jacques''. L'exercice consiste à proposer l'écriture des trois autres voix en respectant les règles énoncées ci-dessus. Pour simplifier, nous ajoutons les contraintes suivantes : * toutes les voix chantent des blanches ; * nous nous limitons aux accords de quinte (accords de trois sons composés d'une tierce et d'une quinte) sans avoir recours à leurs renversements (accords de sixte, accords de sixte et de quarte). Les notes à gauche de la portée indiquent la tessiture (ou ambitus), l'amplitude que peut chanter la voix. {{clear}} {{boîte déroulante/début|titre=Solution possible}} [[Fichier:Harmonisation possible de frere jacques solution.svg|vignette|Harmonisation possible de ''Frère Jacques'' (solution de l'exercice).]] Il n'y a pas qu'une solution possible. Le premier accord doit contenir un ''do''. Nous sommes manifestement en tonalité de ''do'' majeur, nous proposons de commencer par l'accord parfait de ''do'' majeur, I<sup>5</sup>. Le deuxième accord doit comporter un ''ré''. Si nous utilisons l'accord de quinte de ''ré'', nous allons créer une quinte parallèle. Nous pourrions utiliser un renversement, mais nous nous imposons de chercher un autre accord. Il peut s'agir de l'accord ''si''<sup>5</sup> ''(si-ré-fa)'' ou de l'accord de ''sol''<sup>5</sup> ''(sol-si-ré)''. La dernière solution permet d'utiliser l'accord de dominante qui est un accord important de la tonalité. La règle du plus court chemin imposerait le ''sol'' grave pour la partie de basse, mais cela est proche de la limite du chanteur, nous préférons passer au ''sol'' aigu, plus facile à chanter. Nous vérifions qu'il n'y a pas de quinte parallèle : l'intervalle ascendant ''do-sol'' (basse-alto) devient ''sol-si'' (3<sup>ce</sup>), l'intervalle descendant ''do-sol'' (soprano-alto) devient ''ré-si'' (3<sup>ce</sup>). De la même manière, pour le troisième accord, nous ne pouvons pas passer à un accord de ''la''<sup>5</sup> pour éviter une quinte parallèle. Nous avons le choix entre ''do''<sup>5</sup> ''(do-mi-sol)'' et ''mi''<sup>5</sup> ''(mi-sol-si)''. Nous préférons revenir à l'accord de fondamental, solution très stable (l'enchaînement {{Times New Roman|V}}-{{Times New Roman|I}} formant une cadence parfaite). Pour le quatrième accord, nous pourrions rester sur l'accord parfait de ''do'' mais cela planterait en quelque sorte la fin du morceau puisque l'on resterait sur la cadence parfaite ; or, nous connaissons le morceau et savons qu'il n'est pas fini. Nous choisissons l'accord de ''la''<sup>5</sup> qui est une sixte ascendante ({{Times New Roman|I}}-{{Times New Roman|VI}}). Nos aurions pu répartir les voix différemment. Par exemple : * alto : ''sol''-''si''-''sol''-''do'' ; * ténor : ''mi''-''ré''-''mi''-''mi''. {{boîte déroulante/fin}} [[Fichier:Harmonisation possible de frere jacques.midi|vignette|Fichier son correspondant.]] {{clear}} == Annexe == === Accords en musique classique === Un accord est un ensemble de notes jouées simultanément. Il peut s'agir : * de notes jouées par plusieurs instruments ; * de notes jouées par un même instrument : piano, clavecin, orgue, guitare, harpe (la plupart des instruments à clavier et des instruments à corde). Pour deux notes jouées simultanément, on parle d'intervalle « harmonique » (par opposition à l'intervalle « mélodique » qui concerne les notes jouées successivement). Les notes répétées à différentes octaves ne changent pas la nature de l'accord. La musique classique considère en général des empilements de tierces ; un accord de trois notes sera constitué de deux tierces successives, un accord de quatre notes de trois tierces… Lorsque tous les intervalles sont des intervalles impairs — tierces, quintes, septièmes, neuvièmes, onzièmes, treizièmes… — alors l'accord est dit « à l'état fondamental » (ou encore « primitif » ou « direct »). La note de la plus grave est appelée « fondamentale » de l'accord. Lorsque l'accord comporte un ou des intervalles pairs, l'accord est dit « renversé » ; la note la plus grave est appelée « basse ». De manière plus générale, l'accord est dit à l'état fondamental lorsque la basse est aussi la fondamentale. On a donc un état idéal de l'accord (état canonique) — un empilement strict de tierces — et l'état réel de l'accord — l'empilement des notes réellement jouées, avec d'éventuels redoublements, omissions et inversions ; et seule la basse indique si l'accord est à l'état fondamental ou renversé. Le chiffrage dit de « basse continue » ''({{lang|it|basso continuo}})'' désigne la représentation d'un accord sous la forme d'un ou plusieurs chiffres arabes et éventuellement d'un chiffre romain. ==== Accords de trois notes ==== En musique classique, les seuls accords considérés comme parfaitement consonants, c'est-à-dire sonnant agréablement à l'oreille, sont appelés « accords parfaits ». Si l'on prend une tonalité et un mode donné, alors l'accord construit par superposition es degrés I, III et V de cette gamme porte le nom de la gamme qui l'a généré. [[fichier:Accord do majeur chiffre.svg|vignette|upright=0.5|Accord parfait de ''do'' majeur chiffré.]] Par exemple : * « l'accord parfait de ''do'' majeur » est composé des notes ''do'', ''mi'' et ''sol'' ; * « l'accord parfait de ''la'' mineur » est composé des notes ''la'', ''do'' et ''mi''. Un accord parfait majeur est donc composé, en partant de la fondamentale, d'une tierce majeure et d'une quinte juste. Un accord parfait mineur est composé d'une tierce mineure et d'une quinte juste. L'accord parfait à l'état fondamental est appelé « accord de quinte » et est simplement chiffré « 5 » pour indiquer la quinte. On peut également commencer un accord sur sa deuxième ou sa troisième note, en faisant monter celle(s) qui précède(nt) à l'octave suivante. On parle alors de « renversement d'accord » ou d'accord « renversé ». [[Fichier:Accord do majeur renversements chiffre.svg|vignette|upright=0.75|Accord parfait de ''do'' majeur et ses renversements, chiffrés.]] Par exemple, * le premier renversement de l'accord parfait de ''do'' majeur est :<br /> ''mi'', ''sol'', ''do'' ; * le second renversement de l'accord parfait de do majeur est :<br /> ''sol'', ''do'', ''mi''. Les notes conservent leur nom de « fondamentale », « tierce » et « quinte » malgré le changement d'ordre. La note la plus grave est appelée « basse ». Dans le cas du premier renversement, le deuxième note est la tierce de la basse (la note la plus grave) et la troisième note est la sixte ; le chiffrage en chiffres arabes est donc « 6 » (puisque l'on omet la tierce) et l'accord est appelé « accord de sixte ». Pour le deuxième renversement, les intervalles sont la quarte et la sixte, le chiffrage est donc « 6-4 » et l'accord est appelé « accord de sixte et de quarte ». Dans tous les cas, on chiffre le degré on considérant la fondamentale, par exemple {{Times New Roman|I}} si l'accord est construit sur la tonique de la gamme. Les autres accords de trois notes que l'on rencontre sont : * l'accord de quinte diminuée, constitué d'une tierce mineure et d'une quinte diminuée ; lorsqu'il est construit sur le septième degré d'une gamme, on considère que c'est un accord de septième de dominante sans fondamentale (voir plus bas), le degré est donc indiqué « “{{Times New Roman|V}}” » (cinq entre guillemets) et non « {{Times New Roman|VII}} » ; * l'accord de quinte augmenté : il est composé d'une tierce majeure et qu'une quinte augmentée. Dans le tableau ci-dessous, * « m » désigne un intervalle mineur ; * « M » un intervalle majeur ou le mode majeur ; * « J » un intervalle juste ; * « d » un intervalle diminué ; * « A » un intervalle augmenté ; * « mh » le mode mineur harmonique ; * « ma » le mode mineur ascendant ; * « md » le mode mineur descendant. {| class="wikitable" |+ Accords de trois notes ! scope="col" rowspan="2" | Nom ! scope="col" rowspan="2" | 3<sup>ce</sup> ! scope="col" rowspan="2" | 5<sup>te</sup> ! scope="col" rowspan="2" | État fondamental ! scope="col" rowspan="2" | 1<sup>er</sup> renversement ! scope="col" rowspan="2" | 2<sup>nd</sup> renversement ! scope="col" colspan="4"| Construit sur les degrés |- ! scope="col" | M ! scope="col" | mh ! scope="col" | ma ! scope="col" | md |- | Accord parfait<br /> majeur || M || J | accord de quinte || accord de sixte || accord de<br />sixte et de quarte | {{Times New Roman|I, IV, V}} || {{Times New Roman|V, VI}} || {{Times New Roman|IV, V}} || {{Times New Roman|III, VI, VII}} |- | Accord parfait<br /> mineur || m || J | accord de quinte || accord de sixte || accord de<br />sixte et de quarte | {{Times New Roman|II, III, VI}} || {{Times New Roman|I, IV}} || {{Times New Roman|I, II}} || {{Times New Roman|I, IV, V}} |- | Accord de<br />quinte diminuée || m || d | accord de<br />quinte diminuée || accord de<br />sixte sensible<br />sans fondamentale || accord de triton<br />sans fondamentale | {{Times New Roman|VII (“V”)}} || {{Times New Roman|II, VII (“V”)}} || {{Times New Roman|VI, VII (“V”)}} || {{Times New Roman|II}} |- | Accord de<br />quinte augmentée || M || A | accord de<br />quinte augmentée || accord de sixte<br />et de tierce sensible || accord de sixte et de quarte<br />sur sensible | &nbsp; || {{Times New Roman|III}} || {{Times New Roman|III}} || &nbsp; |} ==== Accords de quatre notes ==== Les accords de quatre notes sont des accord composés de trois tierces superposées. La dernière note étant le septième degré de la gamme, on parle aussi d'accords de septième. Ces accords sont dissonants : ils contiennent un intervalle de septième (soit une octave montante suivie d'une seconde descendante). Ils laissent donc une impression de « tension ». Il existe sept différents types d'accords, ou « espèces ». Citons l'accord de septième de dominante, l'accord de septième mineure et l'accord de septième majeure. ===== L'accord de septième de dominante ===== [[Fichier:Accord 7e dominante do majeur renversements chiffre.svg|vignette|Accord de septième de dominante de ''do'' majeur et ses renversements, chiffrés.]] L'accord de septième de dominante est l'empilement de trois tierces à partir de la dominante de la gamme, c'est-à-dire du {{Times New Roman|V}}<sup>e</sup> degré. Par exemple, l'accord de septième de dominante de ''do'' majeur est l'accord ''sol''-''si''-''ré''-''fa'', et l'accord de septième de dominante de ''la'' mineur est ''mi''-''sol''♯-''si''-''ré''. L'accord de septième de dominante dont la fondamentale est ''do'' (''do''-''mi''-''sol''-''si''♭) appartient à la gamme de ''fa'' majeur. Que le mode soit majeur ou mineur, il est composé d'une tierce majeure, d'une quinte juste et d'une septième mineure (c'est un accord parfait majeur auquel on ajoute une septième mineure). C'est de loin l'accord de septième le plus utilisé ; il apparaît au {{pc|xvii}}<sup>e</sup> en musique classique. Dans son état fondamental, son chiffrage est {{Times New Roman|V 7/+}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}). Le signe plus indique la sensible. Son premier renversement est appelé « accord de quinte diminuée et sixte » et est noté {{Times New Roman|V 6/<s>5</s>}} (ou {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}). Son deuxième renversement est appelé « accord de sixte sensible », puisque la sixte de l'accord est la sensible de la gamme, et est noté {{Times New Roman|V +6}} (ou {{Times New Roman|V<sup>+6</sup>}}). Son troisième renversement est appelé « accord de quarte sensible » et est noté {{Times New Roman|V +4}} (ou {{Times New Roman|V<sup>+4</sup>}}). [[Fichier:Accord 7e dominante sans fondamentale do majeur renversements chiffre.svg|vignette|Accord de septième de dominante sans fondamentale de ''do'' majeur et ses renversements, chiffrés.]] On utilise aussi l'accord de septième de dominante sans fondamentale ; c'est alors un accord de trois notes. Dans son état fondamental, c'est un « accord de quinte diminuée » placé sur le {{Times New Roman|VII}}<sup>e</sup> degré (mais c'est bien un accord construit sur le {{Times New Roman|V}}<sup>e</sup> degré), noté {{Times New Roman|“V” <s>5</s>}} (ou {{Times New Roman|“V”<sup><s>5</s></sup>}}). Notez les guillemets qui indiquent que la fondamentale V est absente. Dans son premier renversement, c'est un « accord de sixte sensible sans fondamentale » noté {{Times New Roman|“V” +6/3}} (ou {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}}). Dans son second renversement, c'est un « accord de triton sans fondamentale » (puisque le premier intervalle est une quarte augmentée qui comporte trois tons) noté {{Times New Roman|“V” 6/+4}} (ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}). Notons qu'un accord de septième de dominante n'a pas toujours la dominante pour fondamentale : tout accord composé d'une tierce majeure, d'une quinte juste et d'une septième mineure est un accord de septième de dominante et est chiffré {{Times New Roman|<sup>7</sup><sub>+</sub>}}, quel que soit le degré sur lequel il est bâti (certaines notes peuvent avoir une altération accidentelle). ===== Les accords de septième d'espèce ===== Les autres accords de septièmes sont dits « d'espèce ». L'accord de septième mineure est l'accord de septième formé sur la fondamentale d'une gamme mineure ''naturelle''. Par exemple, l'accord de septième mineure de ''la'' est ''la''-''do''-''mi''-''sol''. Il est composé d'une tierce mineure, d'une quinte juste et d'une septième mineure (c'est un accord parfait mineur auquel on ajoute une septième mineure). L'accord de septième majeure est l'accord de septième formé sur la fondamentale d'une gamme majeure. Par exemple, L'accord de septième majeure de ''do'' est ''do''-''mi''-''sol''-''si''. Il est composé d'une tierce majeure, d'une quinte juste et d'une septième majeure (c'est un accord parfait majeur auquel on ajoute une septième majeure). ==== Utilisation du chiffrage ==== Le chiffrage est utilisé de deux manières. La première manière, c'est la notation de la basse continue. La basse continue est une technique d'improvisation utilisée dans le baroque pour l'accompagnement d'instruments solistes. Sur la partition, on indique en général la note de basse de l'accord et le chiffrage en chiffres arabes. La seconde manière, c'est pour l'analyse d'une partition. Le fait de chiffrer les accords permet de mieux en comprendre la structure. De manière générale, on peut retenir que : * le chiffrage « 5 » indique un accord parfait, superposition d'une tierce (majeure ou mineure) et d'une quinte juste ; * le chiffrage « 6 » indique le premier renversement d'un accord parfait ; * le chiffrage « 6/4 » indique le second renversement d'un accord parfait ; * chiffrage « 7/+ » indique un accord de septième de dominante ; * le signe « + » indique en général que la note de l'intervalle est la sensible ; * un intervalle barré désigne un intervalle diminué. [[fichier:Accords gamme do majeur la mineur.svg|class=transparent| center | Principaux accords construits sur les gammes de ''do'' majeur et de ''la'' mineur harmonique.]] === Notation « jazz » === En jazz et de manière générale en musique rock et populaire, la base d'un accord est la triade composée d'une tierce (majeure ou mineure) et d'une quinte juste. Pour désigner un accord, on utilise la note fondamentale, éventuellement désigné par une lettre dans le système anglo-saxon (A pour ''la'' etc.), suivi d'une qualité (comme « m », « + »…). Les renversements ne sont pas notés de manière particulière, ils sont notés comme les formes fondamentales. Dans les deux tableaux suivants, la fondamentale est notée X (remplace le C pour un accord de ''do'', le D pour un accord de ''ré''…). La construction des accords est décrite par la suite. [[Fichier:Arbre accords triades 5d5J5A.svg|vignette|upright=1.5|Formation des triades présentée sous forme d'arbre.]] {| class="wikitable" |+ Notation des principales triades |- | ! scope="col" | Tierce<br />mineure (3m) ! scope="col" | Tierce<br />majeure (3M) |- ! scope="row" | Quinte diminuée (5d) | X<sup>o</sup>, Xm<sup>♭5</sup>, X–<sup>♭5</sup> || |- ! scope="row" | Quinte juste (5J) | Xm, X– || X |- ! scope="row" | Quinte augmentée (5A) | || X+, X<sup>♯5</sup> |} [[Fichier:Triades do.svg|class=transparent|center|Triades de do.]] {| class="wikitable" |+ Notation des principaux accords de septième |- | colspan="2" | ! scope="col" | Tierce<br />mineure (3m) ! scope="col" | Tierce<br />majeure (3M) |- ! scope="row" rowspan="2" | Quinte<br />diminuée (5d) ! scope="row" | Septième diminuée (7d) | X<sup>o7</sup> || |- ! scope="row" | Septième mineure (7m) | Xm<sup>7(♭5)</sup>, X–<sup>7(♭5)</sup>, X<sup>Ø</sup> || |- ! scope="row" rowspan="3" | Quinte<br />juste (5J) ! scope="row" | Sixte majeure (6M) | Xm<sup>6</sup> || X<sup>6</sup> |- ! scope="row" | Septième mineure (7m) | Xm<sup>7</sup>, X–<sup>7</sup> || X<sup>7</sup> |- ! scope="row" | Septième majeure (7M) | Xm<sup>maj7</sup>, X–<sup>maj7</sup>, Xm<sup>Δ</sup>, X–<sup>Δ</sup> || X<sup>maj7</sup>, X<sup>Δ</sup> |- ! scope="row" rowspan="2" | Quinte<br />augmentée (5A) ! scope="row" | Septième mineure (7m) | || X+<sup>7</sup> |- ! scope="row" | Septième majeure (7M) | || X+<sup>maj7</sup> |} [[Fichier:Arbre accords septieme.svg|class=transparent|center|Formation des accords de septième présentée sous forme d'arbre.]] &nbsp; &nbsp; [[Fichier:Accords do septieme.svg|class=transparent|center|Accord de do septième.]] On notera que l'intervalle de sixte majeure est l'enharmonique de celui de septième diminuée (6M = 7d). [[File:Principaux accords do.svg|class=transparent|center|Principaux accords de do.]] ==== Triades ==== ; Accords fondés sur une tierce majeure * accord parfait majeur : pas de notation *: p. ex. « ''do'' » ou « C » pour l'accord parfait de ''do'' majeur (''do'' - ''mi'' - ''sol'') ; Accords fondés sur une tierce mineure * accord parfait mineur : « m », « min » ou « – » *: « ''do'' m », « ''do'' – », « Cm », « C– »… pour l'accord parfait de ''do'' mineur (''do'' - ''mi''♭ - ''sol'') ==== Triades modifiées ==== ; Accords fondés sur une tierce majeure * accord augmenté (la quinte est augmentée) : aug, +, ♯5 *: « ''do'' aug », « ''do'' + », « ''do''<sup>♯5</sup> » « Caug », « C+ » ou « C<sup>♯5</sup> » pour l'accord de ''do'' augmenté (''do'' - ''mi'' - ''sol''♯) : L'accord augmenté est un empilement de tierces majeures. Ainsi, un accord augmenté a deux notes communes avec deux autres accords augmentés : C+ (''do'' - ''mi'' - ''sol''♯) a deux notes communes avec A♭+ (''la''♭ - ''do'' - ''mi'') et avec E+ (''mi'' - ''sol''♯ - ''si''♯) ; et on remarque que ces trois accords sont en fait enharmoniques (avec les enharmonies ''la''♭ = ''sol''♯ et ''si''♯ = ''do''). En effet, l'octave comporte six tons (sous la forme de cinq tons et deux demi-tons), et une tierce majeure comporte deux tons, on arrive donc à l'octave en ajoutant une tierce majeure à la dernière note de l'accord. ; Accords fondés sur une tierce mineure * accord diminué (la quinte est diminuée) : dim, o, ♭5 *: « ''do'' dim », « ''do''<sup>o</sup> », « ''do''<sup>♭5</sup> », « Cdim », « C<sup>o</sup> » ou « C<sup>♭5</sup> » pour l'accord de ''do'' diminuné (''do'' - ''mi''♭ - ''sol''♭) : On remarque que la quinte diminuée est l'enharmonique de la quarte augmentée et est l'intervalle appelé « triton » (car composé de trois tons). ; Accords fondés sur une tierce majeure ou mineure * accord suspendu de seconde : la tierce est remplacée par une seconde majeure : sus2 *: « ''do''<sup>sus2</sup> » ou « C<sup>sus2</sup> » pour l'accord de ''do'' majeur suspendu de seconde (''do''-''ré''-''sol'') * accord suspendu de quarte : la tierce est remplacée par une quarte juste : sus4 *: « ''do''<sup>sus4</sup> » ou « C<sup>sus4</sup> » pour l'accord de ''do'' majeur suspendu de quarte (''do''-''fa''-''sol'') ==== Triades appauvries ==== ; Accords fondés sur une tierce majeure ou mineure * accord de puissance : la tierce est omise, l'accord n'est constitué que de la fondamentale et de la quinte juste : 5 *: « ''do''<sup>5</sup> », « C<sup>5</sup> » pour l'accord de puissance de ''do'' (''do'' - ''la'') {{note|Très utilisé dans les musiques rock, hard rock et heavy metal, il est souvent joué renversé (''la'' - ''do'') ou bien avec l'ajout de l'octave (''do'' - ''la'' - ''do'').}} ==== Triades enrichies ==== ; Accords fondés sur une tierce majeure * accord de septième (la 7<sup>e</sup> est mineure) : 7 *: « ''do''<sup>7</sup> », « C<sup>7</sup> » pour l'accord de ''do'' septième, appelé « accord de septième de dominante de ''fa'' majeur » en musique classique (''do'' - ''mi'' - ''sol'' - ''si''♭) * accord de septième majeure : Δ, 7M ou maj7 *: « ''do'' <sup>Δ</sup> », « ''do'' <sup>maj7</sup> », « C<sup>Δ</sup> », « C<sup>7M</sup> »… pour l'accord de ''do'' septième majeure (''do'' - ''mi'' - ''sol'' - ''si'') ; Accords fondés sur une tierce mineure * accord de mineur septième (la tierce et la 7<sup>e</sup> sont mineures) : m7, min7 ou –7 *: « ''do'' m<sup>7</sup> », « ''do'' –<sup>7</sup> », « Cm<sup>7</sup> », « C–<sup>7</sup> »… pour l'accord de ''do'' mineur septième, appelé « accord de septième de dominante de ''fa'' mineur » en musique classique (''do'' - ''mi''♭ - ''sol'' - ''si''♭) * accord mineure septième majeure : m7M, m7maj, mΔ, –7M, –7maj, –Δ *: « ''do'' m<sup>7M</sup> », « ''do'' m<sup>maj7</sup> », « ''do'' –<sup>Δ</sup> », « Cm<sup>7M</sup> », « Cm<sup>maj7</sup> », « C–<sup>Δ</sup> »… pour l'accord de ''do'' mineur septième majeure (''do'' - ''mi''♭ - ''sol'' - ''si'') * accord de septième diminué (la quinte et la septième sont diminuée) : dim 7 ou o7 *: « ''do'' dim<sup>7</sup> », « ''do''<sup>o7</sup> », « Cdim<sup>7</sup> » ou « C<sup>o7</sup> » pour l'accord de ''do'' septième diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si''♭) * accord demi-diminué (seule la quinte est diminuée, la septième est mineure) : Ø ou –7(♭5) *: « ''do''<sup>Ø</sup> », « ''do''<sup>7(♭5)</sup> », « C<sup>Ø</sup> » ou « C<sup>7♭5</sup> » pour l'accord de ''do'' demi-diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si'') === Construction pythagoricienne des accords === Nous avons vu au débuts que lorsque l'on joue deux notes en même temps, leurs vibrations se superposent. Certaines superpositions créent un phénomène de battement désagréable, c'est le cas des secondes. Dans le cas d'une tierce majeure, les fréquences des notes quadruple et quintuple d'une même base : les fréquences s'écrivent 4׃<sub>0</sub> et 5׃<sub>0</sub>. Cette superposition de vibrations est agréable à l'oreille. Nous avons également vu que dans le cas d'une quinte juste, les fréquences sont le double et le triple d'une même base, ou encore le quadruple et sextuple si l'on considère la moitié de cette base. Ainsi, dans un accord parfait majeur, les fréquences des fondamentales des notes sont dans un rapport 4, 5, 6. De même, dans le cas d'un accord parfait mineur, les proportions sont de 1/6, 1/5 et 1/4. {{voir|[[../Caractéristiques_et_notation_des_sons_musicaux#Construction_pythagoricienne_et_gamme_de_sept_tons|Caractéristiques et notation des sons musicaux &gt; Construction pythagoricienne et gamme de sept tons]]}} === Un peu de physique : interférences === Les sons sont des vibrations. Lorsque l'on émet deux sons ou plus simultanément, les vibrations se superposent, on parle en physique « d'interférences ». Le modèle le plus simple pour décrire une vibration est la [[w:fr:Fonction sinus|fonction sinus]] : la pression de l'air P varie en fonction du temps ''t'' (en secondes, s), et l'on a pour un son « pur » : : P(''t'') ≈ sin(2π⋅ƒ⋅''t'') où ƒ est la fréquence (en hertz, Hz) du son. Si l'on émet deux sons de fréquence respective ƒ<sub>1</sub> et ƒ<sub>2</sub>, alors la pression vaut : : P(''t'') ≈ sin(2π⋅ƒ<sub>1</sub>⋅''t'') + sin(2π⋅ƒ<sub>2</sub>⋅''t''). Nous avons ici une [[w:fr:Identité trigonométrique#Transformation_de_sommes_en_produits,_ou_antilinéarisation|identité trigonométrique]] dite « antilinéarisation » : : <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{f_1 + f_2}{2}t \right ) \cdot \sin \left ( 2\pi \frac{f_1 - f_2}{2}t \right ).</math> On peut étudier simplement deux situations simples. [[Fichier:Battements interferentiels.png|vignette|Deux sons de fréquences proches créent des battements : la superposition d'une fréquence et d'une enveloppe.]] La première, c'est quand les fréquences ƒ<sub>1</sub> et ƒ<sub>2</sub> sont très proches. Alors, la moyenne (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2 est très proche de ƒ<sub>1</sub> et ƒ<sub>2</sub> ; et la demie différence (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2 est très proche de zéro. On a donc une enveloppe de fréquence très faible, (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2, dans laquelle s'inscrit un son de fréquence moyenne, (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2. C'est cette enveloppe de fréquence très faible qui crée les battements, désagréables à l'oreille. Sur l'image ci-contre, le premier trait rouge montre un instant où les vibrations sont opposées ; elles s'annulent, le son s'éteint. Le second trait rouge montre un instant où les vibrations sont en phase : elle s'ajoutent, le son est au plus fort. {{clear}} La seconde, c'est lorsque les deux fréquences sont des multiples entiers d'une même fréquence fondamentale ƒ<sub>0</sub> : ƒ<sub>1</sub> = ''n''<sub>1</sub>⋅ƒ<sub>0</sub> et ƒ<sub>0</sub> = ''n''<sub>0</sub>⋅ƒ<sub>0</sub>. On a alors : : <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{n_1 + n_2}{2}f_0 \cdot t \right ) \cdot \sin \left ( 2\pi \frac{n_1 - n_2}{2}f_0 \cdot t \right ).</math> On multiplie donc deux fonctions qui ont des fréquences multiples de ƒ<sub>0</sub>. La différence minimale entre ''n''<sub>1</sub> et ''n''<sub>2</sub> vaut 1 ; on a donc une enveloppe dont la fréquence est au minimum la moitié de ƒ<sub>0</sub>, c'est-à-dire un son une octave en dessous de ƒ<sub>0</sub>. Donc, cette enveloppe ne crée pas d'effet de battement, ou plutôt, le battement est trop rapide pour être perçu comme tel. Dans cette enveloppe, on a une fonction sinus dont la fréquence est également un multiple de ƒ<sub>0</sub> ; l'enveloppe et la fonction qui y est inscrite ont donc de nombreux « points communs », d'où l'effet harmonieux. === Le tonnetz === [[File:Speculum musicae.png|thumb|right|225px|Euler, ''De harmoniæ veris principiis'', 1774, p. 350.]] En allemand, le terme ''Tonnetz'' (se prononce « tône-netz ») signifie « réseau tonal ». C'est une représentation graphique des notes qui a été imaginée par [[w:Leonhard Euler|Leonhard Euler]] en 1739. Cette représentation graphique peut aider à la mémorisation de certains concepts de l'harmonie. Cependant, son application est très limitée : elle ne concerne que l'intonation juste d'une part, et que les accords parfait des tonalités majeures et mineures naturelles d'autre part. La représentation contenant les douze notes de la musique savante occidentale, on peut bien sûr représenter d'autres objets, comme les accords de septième ou les accords diminués, mais la représentation graphique est alors compliquée et perd son intérêt pédagogique. On part d'une note, par exemple le ''do''. Si on progresse vers la droite, on monte d'une quinte juste, donc ''sol'' ; vers la gauche, on descend d'une quinte juste, donc ''fa''. Si on va vers le bas, on monte d'une tierce majeure, donc ''mi'' ; si on va vers le haut, on descend d'une tierce majeure, donc ''la''♭ ou ''sol''♯ fa — do — sol — ré | | | | la — mi — si — fa♯ | | | | do♯ — sol♯ — ré♯ — si♭ La figure forme donc un filet, un réseau. On voit que ce réseau « boucle » : si on descend depuis le ''do''♯, on monte d'une tierce majeure, on obtient un ''mi''♯ qui est l'enharmonique du ''fa'' qui est en haut de la colonne. Si on va vers la droite à partir du ''ré'', on obtient le ''la'' qui est au début de la ligne suivante. Si on ajoute des diagonales allant vers la droite et le haut « / », on met en évidence des tierces mineures : ''la'' - ''do'', ''mi'' - ''sol'', ''si'' - ''ré'', ''do''♯ - ''mi''… fa — do — sol — ré | / | / | / | la — mi — si — fa♯ | / | / | / | do♯ — sol♯ — ré♯ — si♭ Donc les liens représentent : * | : tierce majeure ; * — : quinte juste ; * / : tierce mineure. [[Fichier:Tonnetz carre accords fr.svg|thumb|Tonnetz avec les accords parfaits. Les notes sont en notation italienne et les accords en notation jazz.]] On met ainsi en évidence des triangles dont un côté est une quinte juste, un côté une tierce majeure et un côté une tierce mineure ; c'est-à-dire que les notes aux sommets du triangle forment un accord parfait majeur (par exemple ''do'' - ''mi'' - ''sol'') : <div style="font-family:courier; background-color:#fafafa"> fa&nbsp;&nbsp;—&nbsp;'''do&nbsp;&nbsp;&nbsp;—&nbsp;sol'''&nbsp;—&nbsp;ré<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;'''|&nbsp;&nbsp;&nbsp;/'''&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;|<br /> la&nbsp;&nbsp;—&nbsp;'''mi'''&nbsp;&nbsp;&nbsp;—&nbsp;si&nbsp;&nbsp;—&nbsp;fa♯<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;/&nbsp;&nbsp;|<br /> do♯&nbsp;—&nbsp;sol♯&nbsp;—&nbsp;ré♯&nbsp;—&nbsp;si♭ </div> ou un accord parfait mineur (''la'' - ''do'' - ''mi''). <div style="font-family:courier; background-color:#fafafa"> fa&nbsp;&nbsp;—&nbsp;'''do'''&nbsp;&nbsp;&nbsp;—&nbsp;sol&nbsp;—&nbsp;ré<br /> |&nbsp;&nbsp;'''/&nbsp;&nbsp;|'''&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;|<br /> '''la&nbsp;&nbsp;—&nbsp;mi'''&nbsp;&nbsp;&nbsp;—&nbsp;si&nbsp;&nbsp;—&nbsp;fa♯<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;/&nbsp;&nbsp;|<br /> do♯&nbsp;—&nbsp;sol♯&nbsp;—&nbsp;ré♯&nbsp;—&nbsp;si♭ </div> Un triangle représente donc un accord, et un sommet représente une note. Si on passe d'un triangle à un triangle voisin, alors on passe d'un accord à un autre accord, les deux accords ayant deux notes en commun. Ceci illustre la notion de « plus court chemin » en harmonie : si on passe d'un accord à un autre en gardant un côté commun, alors on a un mouvement conjoint sur une seule des trois voix. Par rapport à l'harmonie fonctionnelle : les accords sont contigus à leur fonction, par exemple en ''do'' majeur : * fonction de tonique ({{Times New Roman|I}}) : C, A– et E– sont contigus ; * fonction de sous-dominante ({{Times New Roman|IV}}) : F et D– sont contigus ; * fonction de dominante ({{Times New Roman|V}}) : G et B<sup>o</sup> sont contigus. On notera que les triangles d'un schéma ''tonnetz'' ne représentent que des accords parfaits. Pour représenter un accord de quinte diminuée (''si'' - ''ré'' - ''fa'') ou les accords de septième, en particulier l'accord de septième de dominante, il faut étendre le ''tonnetz'' et l'on obtient des figures différentes. Par ailleurs, il est adapté à ce que l'on appelle « l'intonation juste », puisque tous les intervalles sont idéaux. [[Fichier:Tonnetz carre accords etendu fr.svg|vignette|Tonnetz étendu.]] [[Fichier:Tonnetz carre do majeur accords fr.svg|vignette|Tonnetz de la tonalité de ''do'' majeur. La représentation de l'accord de quinte diminuée sur ''si'' (B<sup>o</sup>) est une ligne et non un triangle.]] [[Fichier:Tonnetz carre do mineur accords fr.svg|vignette|Tonnetz des tonalités de ''do'' mineur naturel (haut) et ''do'' mineur harmonique (bas).]] Si l'on étend un peu le réseau : ré♭ — la♭ — mi♭ — si♭ — fa | / | / | / | / | fa — do — sol — ré — la | / | / | / | / | la — mi — si — fa♯ — do♯ | / | / | / | / | do♯ — sol♯ — ré♯ — la♯ — mi♯ | / | / | / | / | mi♯ — do — sol — ré — la on peut donc trouver des chemins permettant de représenter les accords de septième de dominante (par exemple en ''do'' majeur, G<sup>7</sup>) fa / sol — ré | / si et des accords de quinte diminuée (en ''do'' majeur : B<sup>o</sup>) fa / ré / si Une gamme majeure ou mineure naturelle peut se représenter par un trapèze rectangle : ''do'' majeur fa — do — sol — ré | / la — mi — si et ''do'' mineur la♭ — mi♭ — si♭ / | fa — do — sol — ré En revanche, la représentation d'une tonalité nécessite d'étendre le réseau afin de pouvoir faire figurer tous les accords, deux notes sont représentées deux fois. La représentation des tonalités mineures harmoniques prend une forme biscornue, ce qui nuit à l'intérêt pédagogique de la représentation. [[Fichier:Neo-Riemannian Tonnetz.svg|vignette|upright=2|Tonnetz avec des triangles équilatéraux.]] On peut réorganiser le schéma en décalant les lignes, afin d'avoir des triangles équilatéraux. Sur la figure ci-contre (en notation anglo-saxonne) : * si on monte en allant vers la droite « / », on a une tierce mineure ; * si on descend en allant vers la droite « \ », on a une tierce majeure ; * les liens horizontaux « — » représentent toujours des quintes justes * les triangles pointe en haut sont des accords parfaits mineurs ; * les triangles pointe en bas sont des accords parfaits majeurs. On a alors les accords de septième de dominante F / G — D \ / B et de quinte diminuée F / D / B les tonalités majeures F — C — G — D \ / A — E — B et les tonalités mineures naturelles A♭ — E♭ — B♭ / \ F — C — G — D == Notes et références == {{références}} == Voir aussi == === Bibliographie === * {{ouvrage |prénom1=Francesco |nom1=Gasparini |titre=L'Armonico Pratico al Cimbalo |éditeur=Giusseppe Antonio Silvani |lieu=Bologne |année=1772 |lang=it |url=https://imslp.org/wiki/L'Armonico_Pratico_al_Cimbalo_(Gasparini%2C_Francesco)}} === Liens externes === {{wikipédia|Consonance (harmonie tonale)}} {{wikipédia|Disposition de l'accord}} {{wikisource|Petit Manuel d’harmonie}} * {{lien web | url = https://www.apprendrelesolfege.com/chiffrage-d-accords | titre = Chiffrage d'accords (classique) | site = Apprendrelesolfege.com | consulté le = 2020-12-03 }} * {{lien web | url = https://www.coursd-harmonie.fr/introduction/introduction2_le_chiffrage_d_accords.php | titre = Introduction II : Le chiffrage d'accords | site = Cours d'harmonie.fr | consulté le = 2021-12-14 }} * {{lien web | url=https://www.coursd-harmonie.fr/ | titre = Cours d'harmonie en ligne | auteur = Jean-Baptiste Voinet | site=coursd-harmonie.fr | consulté le = 2021-12-20 }} * {{lien web | url=http://e-harmonie.e-monsite.com/ | titre = Cours d'harmonie classique en ligne | auteur = Olivier Miquel | site=e-harmonie | consulté le = 2021-12-24 }} * {{lien web | url=https://fr.audiofanzine.com/theorie-musicale/editorial/dossiers/les-gammes-et-les-modes.html | titre = Les bases de l’harmonie | site = AudioFanzine | date = 2013-07-23 | consulté le = 2024-01-12 }} * {{lien web | url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/l-accord-de-tristan-dechiffre-et-desacralise-2481605 | titre = L’Accord de Tristan déchiffré et désacralisé ! | site = France Musique | date = 2025-10-22 | consulté le = 2025-10-23 }} * {{lien web | url=https://www.youtube.com/watch?v=X62SAnyfQYw | titre = Qu'est-ce que la basse continue dans la musique baroque ? | auteur = Clément Rochefort | site = YouTube/France Musique | date = 2022-05-02 | consulté le = 2026-08-04 }} ---- ''[[../Mélodie|Mélodie]]'' &lt; [[../|↑]] &gt; ''[[../Représentation musicale|Représentation musicale]]'' [[Catégorie:Formation musicale (livre)|Harmonie]] 5yq0lt8t4js4nqyxqrb20s3ev8an90h 773909 773908 2026-10-02T08:35:02Z Cdang 1202 /* Les accords en général */ orth, voc 773909 wikitext text/x-wiki {{Bases de solfège}} <span style="font-size:25px;">6. Harmonie</span> L'harmonie désigne les notes jouées en même temps, soit plusieurs instruments jouant chacun une note, soit un instrument jouant un accord (instrument dit polyphonique). == Première approche == L'exemple le plus simple d'harmonie est sans doute la chanson en canon : c'est un chant polyphonique, c'est-à-dire à plusieurs voix, chaque voix chantant la même chose en décalé. Prenons par exemple ''Vent frais, vent du matin'' (la version originale est ''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609) : [[Fichier:Vent frais vent du matin.svg|class=transparent|center|Partition de ''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]] [[Fichier:Vent frais vent du matin.midi|vignette|''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]] nous voyons que les voix se superposent de manière « harmonieuse ». Les notes de chaque voix se correspondent point par point (avec un retard), c'est donc un type d'harmonie polyphonique appelé « contrepoint ». Considérons la première note de la mesure 6 pour chaque voix. Nous avons la superposition des notes ''ré''-''fa''-''la'' (du grave vers l'aigu) ; la superposition de notes jouées ou chantées ensembles s'appelle un accord. Cet accord ''ré''-''fa''-''la'' porte le nom « d'accord parfait de ''ré'' mineur » : * « ''ré'' » car la note fondamentale est un ''ré'' ; * « parfait » car il est l'association d'une tierce, ''ré''-''fa'', et d'une quinte juste, ''ré''-''la'' ; * « mineur » car le premier intervalle, ''ré''-''fa'', est une tierce mineure. Considérons maintenant un chant accompagné au piano. Le piano peut jouer plusieurs notes en même temps, il peut jouer des accords. [[Fichier:Au clair de le lune chant et piano.svg|class=transparent|center|Deux premières mesure d’Au clair de la lune.]] [[Fichier:Au clair de le lune chant et piano.midi|vignette|Deux premières mesure d’Au clair de la lune.]] L'accord, les notes à jouer simultanément, sont écrites « en colonne ». Lorsqu'on les énonce, on les lit de bas en haut mais le pianiste les joue en pressant les touches du clavier en même temps, de manière « plaquée ». Le premier accord est composé des notes ''do''-''mi''-''sol'' ; il est appelé « accord parfait de ''do'' majeur » car la note fondamentale est ''do'', qu'il est l'association d'une tierce et d'une quinte juste et que le premier intervalle, ''do''-''mi'', est une tierce majeure. Si l'on lit toutes les notes, de la plus grave à la plus aiguë, nous avons : ''do'' (à la clef de ''fa'', main gauche du piano) - ''do'' - ''mi'' - ''sol'' (main droite du piano) - ''do'' (chant) ; nous voyons que la note ''do'' est jouée plusieurs fois, cela ne change pas la qualité de l'accord. == Consonance et dissonance == [[Fichier:Seconde majeure septieme mineure.svg|vignette|Seconde majeure et son renversement, le septième mineure.]] Les notions de consonance et de dissonance sont culturelles et changent selon l'époque. Nous pouvons néanmoins noter que : * l'accord de seconde, et son renversement la septième, créent des battements, les notes « frottent », c'est un intervalle harmonique dissonant ; mais dans le cas de la septième, comme les notes sont éloignées, le frottement est moins perceptible ; * les accords d'unisson, de tierce, quarte et quinte sonnent agréablement à l'oreille, ils sont consonants. Dans la musique savante européenne, au début du Moyen-Âge, seuls les accords d'unisson, de quarte et de quinte étaient considérés comme consonants, d'où leur qualification de « juste ». La tierce, et son renversement la sixte, étaient perçues comme dissonantes ; à partir de la Renaissance, ces deux intervalles sont considérés comme consonants. L'harmonie joue avec les consonances et les dissonances. Dans un premier temps, les harmonies dissonantes sont utilisées pour créer des tensions qui sont ensuite résolues, on utilise des successions « consonant-dissonant-consonant ». À force d'entendre des intervalles considérés comme dissonants, l'oreille s'habitue et certains finissent par être considérés comme consonants ; c'est ce qui est arrivé à la tierce et à la sixte à la fin du Moyen Âge avec le contrepoint, puis à la septième plus tardivement. Il faut ici aborder la notion d'harmonique des notes. [[File:Harmoniques de do.svg|thumb|Les six premières harmoniques de ''do''.]] Lorsque l'on joue une note, on entend d'autres notes plus aigües et plus faibles ; la note jouée est appelée la « fondamentale » et les notes plus aigües et plus faibles sont les « harmoniques ». C'est cette accumulation d'harmoniques qui donne la couleur au son, son timbre, qui fait qu'un piano ne sonne pas comme un violon. Par exemple, si l'on joue un ''do''<sup>1</sup><ref>Pour la notation des octaves, voir ''[[../Représentation_musicale#Désignation_des_octaves|Représentation musicale &gt; Désignation des octaves]]''.</ref> (fondamentale), on entend le ''do''<sup>2</sup> (une octave plus aigu), puis un ''sol''<sup>2</sup>, puis encore un ''do''<sup>3</sup> plus aigu, puis un ''mi''<sup>3</sup>, puis encore un ''sol''<sup>3</sup>, puis un ''si''♭<sup>3</sup>… Ainsi, puisque lorsque l'on joue un ''do'' on entend aussi un ''sol'' très léger, alors jouer un ''do'' et un ''sol'' simultanément n'est pas choquant. De même pour ''do'' et ''mi''. De là vient la notion de consonance. Le statut du ''si''♭ est plus ambigu. Il fait partie des harmoniques qui sonnent naturellement, mais il forme une seconde descendante avec le ''do'', intervalle dissonant. Par ailleurs, on remarque que le ''si''♭ ne fait pas partie de la gamme de ''do'' majeur, contrairement au ''sol'' et au ''mi''. Pour le jeu sur les dissonances, on peut écouter par exemple la ''Toccata'' en ''ré'' mineur, op. 11 de Sergueï Prokofiev (1912). : {{lien web |url=https://www.youtube.com/watch?v=AVpnr8dI_50 |titre=Yuja Wang Prokofiev Toccata |site=YouTube |date=2019-02-26 |consulté le=2021-12-19}} == Contrepoint == Dans le chant grégorien, la notion d'accord n'existe pas. L'harmonie provient de la superposition de plusieurs mélodies, notamment dans ce que l'on appelle le « contrepoint ». Le terme provient du latin ''« punctum contra punctum »'', littéralement « point par point », et désigne le fait que les notes de chaque voix se correspondent. L'exemple le plus connu de contrepoint est le canon, comme ''Frère Jacques'' ou bien ''Vent frais, vent du matin'' présenté ci-dessus : chaque note d'un couplet correspond à une note du couplet précédent. Certains morceaux sont bâtis sur une écriture « en miroir » : l'ordre des notes est inversé entre les deux voix, ou bien les intervalles sont inversés (« mouvement contraire » : une tierce montante sur une voix correspond à une tierce descendante sur l'autre). On peut également citer le « mouvement oblique » (une des voix, le bourdon, chante toujours la même note) et le mouvement parallèle (les deux voix chantent le même air mais transposé, l'une est plus aiguë que l'autre). Nous reproduisons ci-dessous le début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel. [[Fichier:Haendel Sonate en trio re mineur debut canon.svg | vignette | center | upright=2 | Début du second ''Allergo'' de la sonate en trio en ''ré'' mineur de Haendel.]] [[Fichier:Haendel Sonate en trio re mineur debut.midi | vignette | Début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.]] Nous avons mis en évidence la construction en canon avec des encadrés de couleur : sur les quatre premières mesures, nous voyons trois motifs repris alternativement par une voix et par l'autre. Ce type de procédé est très courant dans la musique baroque. Les procédés du contrepoint s'appliquent également à la danse : * unisson : les danseurs et danseuses font les mêmes gestes en même temps ; * répétition : le fait de répéter une série de gestes, une « phrase dansante » ; * canon : les gestes sont faits avec un décalage régulier d'un danseur ou d'une danseuse à l'autre ; * cascade : forme de canon dans laquelle le décalage est très petit ; * contraste : deux danseur·euses, ou deux groupes, ont des gestuelles très différentes ; * accumulation : la gestuelle se complexifie par l'ajout d'éléments au fur et à mesure ; ou bien le nombre de danseur·euses augmente ; * dialogue : les gestes de danseur·euses ou de groupes se répondent ; * contre-point : la gestuelle d'un ou une danseuse se superpose à la gestuelle d'un groupe ; * lâcher-rattraper : les danseurs et danseuses alternent danse à l'unisson et gestuelles indépendantes. : {{lien web | url=https://www.youtube.com/watch?v=wgblAOzedFc | titre=Les procédés de composition en danse | auteur= Doisneau Sport TV | site=YouTube | date=2020-03-16 | consulté le=2021-01-21 }} {{...}} == Ostinato == L'ostinato, mot italien signifiant « obstiné », c'est le fait de jouer en boucle un passage. L'ostinato peut ainsi former une structure autour de laquelle peut se développer le morceau. Lorsque l'ostinato est joué par l'instrument de basse (basson, violoncelle, viole de gambe basse, flûte à bec basse, main gauche du piano…), on parle de basse obstinée. Les grilles d'accord en jazz sont des ostinatos (ou ostinati) joués par la section rythmique. Exemples : * {{lien web |url=https://www.youtube.com/watch?v=QZpGe5rNJkI&t=41s |titre=Epic Patty Cake Song (I'll Think Of You) |auteur= Kurt Hugo Schneider |site=YouTube |date=2014-08-12 |consulté le=2026-01-24}} : un ostinato de percussions corporelles ; * {{lien web |url=https://www.youtube.com/watch?v=Kv-37-nXC48 |titre=Pow Wow ''Le lion est mort ce soir'' Les Victoires de la Musique 1993 |auteur=Solomon Linda |date=2015-02-02 |consulté le=2026-01-24}} : dans ce chant ''a capella'' (sans accompagnement d'instrument), les voix de basse forment une basse obstinée, sur les syllabes « oëap » durant le couplet et « awimbowé » sur le refrain ; * {{lien web |url=https://www.youtube.com/watch?v=JvNQLJ1_HQ0 |titre=Pachelbel Canon in D Major - the original and best version. |auteur=Voices of Music |site=YouTube |date=2008-09-02 |consulté le=2026-01-24}} : le ''Canon de Pachelbel de ''ré'' mineur'' (''Canon per tre Violini e Basso'', Johann Pachelbel, vers 1680) est construit sur une basse obstinée de huit mesures. == Les accords en général == Initialement, on a des chants polyphoniques, des voix qui chantent chacune une mélodie, les mélodies se mêlant. On remarque que certaines superpositions de notes sonnent de manière plus ou moins agréables, consonantes ou dissonantes. On en vient alors à associer ces notes, c'est-à-dire à considérer dès le départ la superposition de ces notes et non pas la rencontre de ces notes au gré des mélodies. Ces groupes de notes superposées forment les accords. En Europe, cette notion apparaît vers le {{pc|xiv}}<sup>e</sup> siècle avec notamment la ''[[wikipedia:fr:Messe de Notre Dame|Messe de Notre Dame]]'' de Guillaume de Machaut (vers 1360-1365). À l'origine, dans les chants polyphoniques, les voix sont d'égale importance. Puis, au cours du temps, une des voix devient plus importante, le « cantus firmus » ; initialement, il s'agit de la voix intermédiaire, le « ténor » ou « taille », puis c'est la voix la plus aigüe, le « soprano » ou « dessus ». À la Renaissance, au {{pc|xvi}}{{e}} siècle, apparaît la notion de « monodie accompagnée » : la voix principale, le cantus firmus, devient la voix mélodique, la monodie, et les autres voix servent à l'accompagner. À l'époque baroque, au {{pc|xvii}}{{e}} siècle, apparaît la notion d'accord, notes sonnant simultanément, avec une notation simplifiée, la « basse chiffrée » : a lieu d'écrire toutes les notes de l'accord, on écrit la basse et des chiffres indiquent quelles sont les autres notes à jouer. La notion « d'accord parfait » est consacrée par [[wikipedia:fr:Jean-Philippe Rameau|Jean-Philippe Rameau]] dans son ''Traité de l'harmonie réduite à ses principes naturels'', publié en 1722. === Qu'est-ce qu'un accord ? === Un accord est un ensemble d'au minimum trois notes jouées en même temps. « Jouées » signifie qu'il faut qu'à un moment donné, elles sonnent en même temps, mais le début ou la fin des notes peut être à des instants différents. Considérons que l'on joue les notes ''do'', ''mi'' et ''sol'' en même temps. Cet accord s'appelle « accord de ''do'' majeur ». En musique classique, on lui adjoint l'adjectif « parfait » : « accord parfait de ''do'' majeur ». Nous représentons ci-dessous trois manière de faire l'accord : avec trois instruments jouant chacun une note : [[Fichier:Do majeur trois portees.svg|class=transparent|center|Accord de ''do'' majeur avec trois instruments différents.]] Avec un seul instrument jouant simultanément les trois notes : [[Fichier:Chord C.svg|class=transparent|center|Accord de ''do'' majeur joué par un seul instrument.]] L'accord tel qu'il est joué habituellement par une guitare d'accompagnement : [[Fichier:Do majeur guitare.svg|class=transparent|center|Accord de ''do'' majeur à la guitare.]] Pour ce dernier, nous représentons le diagramme indiquant la position des doigts sur le manche au dessus de la portée et la tablature en dessous. Ici, c'est au total six notes qui sont jouées : ''mi'' grave, ''do'' médium, ''mi'' médium, ''sol'' médium, ''do'' aigu, ''mi'' aigu. Mais il s'agit bien des trois notes ''do'', ''mi'' et ''sol'' jouées à des octaves différentes. Nous remarquons également que la note de basse (la note la plus grave), ''mi'', est différente de la note fondamentale (celle qui donne le nom à l'accord), ''do'' ; l'accord est dit « renversé » (voir plus loin). === Comment joue-t-on un accord ? === Les notes ne sont pas forcément jouées en même temps ; elles peuvent être « égrainées », jouée successivement, ce que l'on appelle un arpège. La partition ci-dessous montre six manières différentes de jouer un accord de ''la'' mineur à la guitare, plaqué puis arpégé. [[Fichier:La mineur differentes executions.svg|class=transparent|center|Différentes exécution de l'accord de do majeur à la guitare.]] [[Fichier:La mineur differentes executions midi.midi|vignette|Différentes exécution de l'accord de la mineur à la guitare.]] Vous pouvez écouter l'exécution de cette partition avec le lecteur ci-contre. Seuls les instruments polyphoniques peuvent jouer les accords plaqués : instruments à clavier (clavecin, orgue, piano, accordéon), les instruments à plusieurs cordes pincées (harpe, guitare ; violon, alto, violoncelle et contrebasse joués en pizzicati). Les instruments à corde frottés de la famille du violon peuvent jouer des notes par deux à l'archet mais pas plus du fait de la forme bombée du chevalet ; cependant, un mouvement rapide permet de jouer les quatre cordes de manière très rapprochée. Les instruments à percussion de type xylophone ou le tympanon permettent de jouer jusqu'à quatre notes simultanément en tenant deux baguettes (mailloches, maillets) par main. Tous les instruments peuvent jouer des arpèges même si, dans le cas des instruments monodiques, les notes ne continuent pas à sonner lorsque l'on passe à la note suivante. L'arpège peut être joué par l'instrument de basse (basson, violoncelle, contrebasse, guitare basse, pédalier de l'orgue…), notamment dans le cas d'une basse continue ou d'une ''{{lang|en|walking bass}}'' (« basse marchante » : la basse joue des noires, donnant ainsi l'impression qu'elle marche). En jazz, et spécifiquement au piano, on a recours au ''{{lang|en|voicing}}'' : on choisit la manière dont on organise les notes pour donner une couleur spécifique, ou bien pour créer une mélodie en enchaînant les accords. Il est fréquent de ne pas jouer toutes les notes : si on n'en garde que deux, ce sont la tierce et la septième, car ce sont celles qui caractérisent l'accord (selon que la tierce est mineure ou majeure, que la septième est majeure ou mineure), et la fondamentale est en général jouée par la contrebasse ou guitare basse. {{clear}} === Classes d'accord === [[Fichier:Intervalles harmoniques accords classes.svg|vignette|upright=1.5|Intervalles harmoniques dans les accords classés de trois, quatre et cinq notes.]] Un accord composé d'empilement de tierces est appelé « accord classé ». En musique tonale, c'est-à-dire la musique fondée sur les gammes majeures ou mineures (cas majoritaire en musique classique), on distingue trois classes d'accords : * les accords de trois notes, ou triades, ou accords de quinte ; * les accords de quatre notes, ou accords de septième ; * les accords de cinq notes, ou accords de neuvième. En empilant des tierces, si l'on part de la note fondamentale, on a donc de intervalles de tierce, quinte, septième et neuvième. En musique tonale, les accords avec d'autres intervalles (hors renversement, voir ci-après), typiquement seconde, quarte ou sixte, sont considérés comme des transitions entre deux accords classés. Ils sont appelés, selon leur utilisation, « accords à retard » (en anglais : ''{{lang|en|suspended chord}}'', accord suspendu) ou « appoggiature » (note « appuyée », étrangère à l'harmonie). Voir aussi plus loin la notion de note étrangère. === Renversements d'accords === [[File:Accord do majeur renversements.svg|thumb|Accord parfait de do majeur et ses renversements.]] [[Fichier:Progression dominante renverse parfait do majeur.svg|vignette|upright=0.6|Progression accord de dominante renversé → accord parfait en ''do'' majeur.]] Un accord classé est donc un empilement de tierces. Si l'on change l'ordre des notes, on a toujours le même accord mais il est fait avec d'autres intervalles harmoniques. Par exemple, l'accord parfait de ''do'' majeur dans son état fondamental, c'est-à-dire non renversé, s'écrit ''do'' - ''mi'' - ''sol''. Sa note fondamentale, ''do'', est aussi se note de basse. Si maintenant on prend le ''do'' de l'octave supérieure, l'accord devient ''mi - sol - do'' ; c'est l'empilement d'une tierce ''(mi - sol)'' et d'une quarte ''(sol - do)'', soit la superposition d'une tierce ''(mi - sol)'' et d'une sixième ''(mi - do)''. C'est le premier renversement de l'accord parfait de ''do'' majeur ; la fondamentale est toujours ''do'' mais la basse est ''mi''. Le second renversement est ''sol - do - mi''. L'utilisation de renversement peut faciliter l'exécution de la progression d'accord. Par exemple, en tonalité ''do'' majeur, si l'on veut passer de l'accord de dominante ''sol - si - ré'' à l'accord parfait ''do - mi - sol'', alors on peut utiliser le second renversement de l'accord de dominante : ''ré - sol - si'' → ''do - mi - sol''. Ainsi, la basse descend juste d'un ton ''(ré → do)'' et sur un piano, la main reste globalement dans la même position. Le renversement d'un accord permet également de respecter certaines règles de l'harmonie classique, notamment éviter que des voix se suivent strictement (« mouvement parallèle »), ce qui aurait un effet de platitude. De manière générale, la notion de renversement permet deux choses : * d'enrichir l'œuvre : pour créer une harmonie donnée (c'est-à-dire des sons sonnant bien ensemble), nous avons plus de souplesse, nous pouvons organiser ces notes comme nous le voulons selon les voix ; * de simplifier l'analyse : quelle que soit la manière dont sont organisées les notes, cela nous ramène à un même accord. {{citation bloc|Or il, y a plusieurs manières de jouer un accord, selon que l'on aborde par la première note qui le constitue, ''do mi sol'', la deuxième, ''mi sol do'', ou la troisième note, ''sol do mi''. Ce sont les renversements, [que Rameau] va classer en différentes combinaisons d'une seule matrice. Faisant cela, Rameau divise le nombre d'accords [de septième] par quatre. Il simplifie, il structure […].|{{ouvrage|prénom1=André |nom1=Manoukian |titre=Sur les routes de la musique |éditeur=Harper Collins |année=2021 |passage=54 |isbn=979-1-03391201-9}} }} {{clear}} [[File:Plusieurs realisation 1er renversement doM.svg|thumb|Plusieurs réalisation du premier renversement de l'accord de ''do'' majeur.]] Notez que # Les notes peuvent être répétées plusieurs fois, à différentes octaves. # Dans la notion de renversement, seule importe en fait la note de basse. Ainsi, les accords ''mi-sol-do'', ''mi-do-sol'', ''mi-do-mi-sol'', ''mi-sol-mi-do''… sont tous une déclinaison du premier renversement de ''do-mi-sol'' et ils seront abrégés de la même manière (''mi''<sup>6</sup> en musique classique ou C/E en musique populaire et jazz, voir plus bas). {{clear}} == Notation des accords de trois notes == Les accords de trois notes sont appelés « accords de quinte » en classique, et « triades » en jazz. [[Fichier:Progression dominante renverse parfait do majeur chiffrage.svg|vignette|upright=0.7|Chiffrage du second renversement d'un accord de ''sol'' majeur et d'un accord de ''do'' majeur : notation en musique populaire et jazz (haut) et notation de basse chiffrée (bas).]] Les accords sont construits de manière systématique. Nous pouvons donc les représenter de manière simplifiée. Cette notation simplifiée des accords est appelée « chiffrage ». Reprenons la progression d'accords ci-dessus : « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » dans la tonalité de ''do'' majeur. On utilise en général trois notations différentes : * en musique populaire, jazz, rock… un accord est désigné par sa note fondamentale ; ici donc, les accords sont notés « ''sol'' - ''do'' » ou, en notation anglo-saxonne, « G - C » ;<br /> comme le premier accord est renversé, on indique la note de basse après une barre, la progression d'accords est donc chiffrée '''« ''sol''/''ré'' - ''do'' »''' ou '''« G/D - C »''' ;<br /> il s'agit ici d'accords composés d'une tierce majeure et d'une quinte juste ; si les accords sont constitués d'intervalles différents, nous ajoutons un symbole après la note : « m » ou « – » si la tierce est mineure, « dim » ou « ° » si la quinte est diminuée ; * en musique classique, on utilise la notation de « basse chiffrée » (utilisée notamment pour noter la basse continue en musique baroque) : on indique la note de basse sur la portée et on lui adjoint l'intervalle de la fondamentale à la note la plus haute (donc ici respectivement 6 et 5, puisque ''sol''-''si'' est une sixte et ''do''-''sol'' est une quinte), étant sous-entendu que l'on a des empilements de tierce en dessous ; mais dans le cas du premier accord, le premier intervalle n'est pas une tierce, mais une quarte ''(ré''-''sol)'', on note donc '''« ''ré'' <sup>6</sup><sub>4</sub> - ''do'' <sup>5</sup> »'''<ref>quand on ne dispose pas de la notation en supérieur (exposant) et inférieur (indice), on utilise parfois une notation sous forme de fraction : ''sol'' 6/4 et ''do'' 5/.</ref> ; * lorsque l'on fait l'analyse d'un morceau, on s'attache à identifier la note fondamentale de l'accord (qui est différente de la basse dans le cas d'un renversement) ; on indique alors le degré de la fondamentale : '''« {{Times New Roman|V<sup>6</sup><sub>4</sub> - I<sup>5</sup>}} »'''. La notation de basse chiffrée permet de construire l'accord à la volée : * on joue la note indiquée (basse) ; * s'il n'y a pas de 2 ni de 4, on lui ajoute la tierce ; * on ajoute les intervalles indiqués par le chiffrage. La notation de musique jazz oblige à connaître la composition des différents accords, mais une fois que ceux-ci sont acquis, il n'y a pas besoin de reconstruire l'accord. La notation de basse chiffrée avec les chiffres romains n'est pas utilisée pour jouer, mais uniquement pour analyser ; Sur les partitions avec basse chiffrée, il y a simplement les chiffrages indiqués au-dessus de la partie de basse. Le chiffrage avec le degré en chiffres romains présente l'avantage d'être indépendant de la tonalité et donc de se concentrer sur la fonction de l'accord au sein de la tonalité. Par exemple, ci-dessous, nous pouvons parler de la progression d'accords « {{Times New Roman|V - I}} » de manière générale, cette notation étant valable quelle que soit la tonalité. [[File:Progression dominante renverse parfait do majeur chiffrage basse continue.svg|thumb|Chiffrage en notation basse chiffrée de la progression d'accords « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » en do majeur.]] {{note|En notation de base continue avec fondamentale en chiffres romains, la fondamentale est toujours indiquée ''sous'' la portée de la partie de basse. Les intervalles sont indiqués au-dessus de la portée de la partie de basse ; lorsque l'on fait une analyse, on peut ayssi les indiquer à côté du degré en chiffres romains, donc sous la portée de la basse.}} {{note|En notation rock, le 5 en exposant indique un accord incomplet avec uniquement la fondamentale et la quinte, un accord sans tierce appelé « accord de puissance » ou ''{{lang|en|power chord}}''. Par exemple, C<sup>5</sup> est l'accord ''do-sol''.}} {{clear}} [[Fichier:Accords parfait do majeur basse chiffree fondamental et renverse.svg|vignette|upright=2.5|Chiffrage de l'accord parfait de ''do'' majeur en basse chiffrée, à l'état fondamental et ses renversements.]] Concernant les accords parfaits en notation de basse chiffrée : * un accord parfait à l'état fondamental est chiffré « <sup>5</sup> » ; on l'appelle « accord de quinte » ; * le premier renversement est chiffré « <sup>6</sup> » (la tierce est implicite) ; on l'appelle « accord de sixte » ; * le second renversement est noté « <sup>6</sup><sub>4</sub> » ; on l'appelle « accord de sixte et de quarte » (ou bien « de quarte et de sixte »). Par exemple, pour l'accord parfait de ''do'' majeur : * l'état fondamental ''do''-''mi''-''sol'' est noté ''do''<sup>5</sup> ; * le premier renversement ''mi''-''sol''-''do'' est noté ''mi''<sup>6</sup> ; * le second renversement ''sol''-''do''-''mi'' est noté ''sol''<sup>6</sup><sub>4</sub>. Il y a une exception : l'accord construit sur la sensible (7{{e}} degré) contient une quinte diminuée et non une quinte juste. Le chiffrage est donc différent : * l'état fondamental ''si''-''ré''-''fa'' est noté ''si''<sup><s>5</s></sup> (cinq barré), « accord de quinte diminuée » ; * le premier renversement ''ré''-''fa''-''si'' est noté ''ré''<sup>+6</sup><sub>3</sub>, « accord de sixte sensible et tierce » ; * le second renversement ''fa''-''si''-''ré'' est noté ''fa''<sup>6</sup><sub>+4</sub>, « accord de sixte et quarte sensible ». Par ailleurs, on ne considère pas qu'il est fondé sur la sensible, mais sur la dominante. Si certains indiquent le degré {{Times New Roman|VII}}, d'autres indiquent le degré {{Times New Roman|V}} en mettant des guillemets, « {{Times New Roman|“V”}} ». Donc selon l'état, le chiffrage est {{Times New Roman|“V”<sup><s>5</s></sup>}}, {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}} ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}. En notation jazz, on ajoute « dim », « <sup>o</sup> » ou bien « <sup>♭5</sup> » au chiffrage, ici : B dim, B<sup>o</sup> ou B<sup>♭5</sup> pour l'état fondamental. Pour les renversements : B dim/D et B dim/F ; ou bien B<sup>o</sup>/D et B<sup>o</sup>/F ; ou bien B<sup>♭5</sup>/D et B<sup>♭5</sup>/F. {{clear}} [[Fichier:Accords basse chiffree basse do fondamental et renverses.svg|vignette|upright=2|Basse chiffrée : accords de quinte, de sixte et de sixte et de quarte ayant pour basse ''do''.]] Et concernant les accords ayant pour basse ''do'' en tonalité de ''do'' majeur : * l'accord ''do''<sup>5</sup> est un accord à l'état fondamental, c'est donc l'accord ''do''-''mi''-''sol'' (sa fondamentale est ''do'') ; * l'accord ''do''<sup>6</sup> est le premier renversement d'un accord, c'est donc l'accord ''do''-''mi''-''la'' (sa fondamentale est ''la'') ; * l'accord ''do''<sup>6</sup><sub>4</sub> est le second renversement d'un accord, c'est donc l'accord ''do''-''fa''-''la'' (sa fondamentale est ''fa''). {{clear}} == Notes étrangères == La musique européenne s'appuie essentiellement sur des accords parfaits, c'est-à-dire fondés sur une tierce majeure ou mineure, et une quinte juste. Il arrive fréquemment qu'un accord ne soit pas un accord parfait. Les notes qui font partie de l'accord parfait sont appelées « notes naturelles » et la note qui n'en fait pas partie est appelée « note étrangère ». Il existe plusieurs types de notes étrangères : * anticipation : la note étrangère est une note naturelle de l'accord suivant ; * appogiature : note d'ornementation qui se résout par mouvement conjoint, c'est-à-dire qu'elle est suivie par une note située juste au-dessus ou en dessous (seconde ascendante ou descendante) qui est, elle, une note naturelle ; * broderie : on part d'une note naturelle, on monte ou on descend d'une seconde, puis on revient sur la note naturelle ; * double broderie : on part d'une note naturelle, on joue la note du dessus puis la note du dessous avant de revenir à la note naturelle ; ou bien on joue la note du dessous puis la note du dessus ; * échappée : note étrangère n'appartenant à aucune des autres catégories ; * note de passage : mouvement conjoint allant d'une note naturelle d'un accord à une note naturelle de l'accord suivant ; * pédale : la note de basse reste la même pendant plusieurs accords successifs ; * retard : la note étrangère est une note naturelle de l'accord précédent. Les notes étrangères ne sont pas chiffrées. [[File:Notes etrangeres accords.svg|center|Différents types de notes étrangères.]] {{note|Les anglophones distinguent deux types de retard : la ''{{lang|en|suspension}}'' est résolue vers le haut (le mouvement est ascendant), le ''{{lang|en|retardation}}'' est résolu vers le bas (le mouvement est descendant).}} == Principaux accords == Les trois principaux accords sont : * l'accord parfait majeur : il est construit sur les degrés {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (médiante) et {{Times New Roman|V}} (dominante) d'une gamme majeure ; il est noté {{Times New Roman|I}}<sup>5</sup>, {{Times New Roman|IV}}<sup>5</sup>, {{Times New Roman|V}}<sup>5</sup> ; * l'accord parfait mineur : il est construit sur les degrés {{Times New Roman|I}} (tonique) et {{Times New Roman|IV}} (sous-tonique) d'une gamme mineure harmonique ; il est également noté {{Times New Roman|I}}<sup>5</sup> et {{Times New Roman|IV}}<sup>5</sup>, les anglo-saxons le notent {{Times New Roman|i}}<sup>5</sup> et {{Times New Roman|iv}}<sup>5</sup> (la minuscule indiquant le caractère mineur) ; * l'accord de septième de dominante : il est construit sur le degré {{Times New Roman|V}} (dominante) d'une gamme majeure ou mineure harmonique ; il est noté {{Times New Roman|V}}<sup>7</sup><sub>+</sub>. On peut trouver ces trois accords sur d'autres degrés, et il existe d'autre types d'accords. Nous verrons cela plus loin. {| class="wikitable" |+ Constitution des principaux accords — Dénomination classique |- ! scope="col" | Accord ! scope="col" | 1<sup>er</sup> intervalle ! scope="col" | 2<sup>e</sup> intervalle ! scope="col" | 3<sup>e</sup> intervalle |- ! scope="row" | Accord parfait majeur | tierce majeure (3M) || quinte juste (5J) || — |- ! scope="row" | Accord parfait mineur | tierce mineure (3m) || quinte juste (5J) || — |- ! scope="row" | Accord de septième de dominante | tierce majeure (3M) || quinte juste (5J) || septième mineure (7m) |} {| class="wikitable" |+ Constitution des principaux accords — Dénomination jazz |- ! scope="col" | Accord ! scope="col" | 1<sup>er</sup> intervalle ! scope="col" | 2<sup>e</sup> intervalle ! scope="col" | 3<sup>e</sup> intervalle |- ! scope="row" | Triade majeure | tierce majeure (3M) || quinte juste (5J) || — |- ! scope="row" | Triade mineure | tierce mineure (3m) || quinte juste (5J) || — |- ! scope="row" | Accord de septième | tierce majeure (3M) || quinte juste (5J) || septième mineure (7m) |} {| border="0" |- | [[Fichier:Accord do majeur arpege puis plaque.midi | Accord parfait de ''do'' majeur (C).]] || [[Fichier:Accord do mineur arpege puis plaque.midi | Accord parfait de ''do'' mineur (Cm).]] || [[Fichier:Accord do septieme arpege puis plaque.midi | Accord de septième de dominante de ''fa'' majeur (C<sup>7</sup>).]] |- | Accord parfait<br /> de ''do'' majeur (C). || Accord parfait<br /> de ''do'' mineur (Cm). || Accord de septième de dominante<br /> de ''fa'' majeur (C<sup>7</sup>). |} '''Rappel :''' * la tierce mineure est composée d'un ton et demi (1 t ½) ; * la tierce majeur est composée de deux tons (2 t) ; * la quinte juste a la même altération que la fondamentale, sauf lorsque la fondamentale est ''si'' (la quinte juste est alors ''fa''♯) ; * la septième mineure est le renversement de la seconde majeure (1 t). [[File:Renversements accords pft fa maj basse chiffree.svg|thumb|Renversements de l'accord parfait de ''fa'' majeur, et la notation de basse chiffrée.]] [[File:Renversements accord sept de dom fa maj basse chiffree.svg|thumb|Renversements de l'accord de septième de dominante de ''fa'' majeur, et la notation de basse chiffrée.]] {| class="wikitable" |+ Notation des principaux accords en musique classique |- ! scope="col" | Accord ! scope="col" | État<br /> fondamental ! scope="col" | Premier<br /> renversement ! scope="col" | Deuxième<br /> renversement ! scope="col" | Troisième<br /> renversement |- ! scope="row" | Accord parfait | {{Times New Roman|I<sup>5</sup>}}<br/> acc. de quinte || {{Times New Roman|I<sup>6</sup>}}<br :> acc. de sixte || {{Times New Roman|I<sup>6</sup><sub>4</sub>}}<br /> acc. de quarte et de sixte || — |- ! scope="row" | Accord de septième<br /> de dominante | {{Times New Roman|V<sup>7</sup><sub>+</sub>}}<br /> acc.de septième de dominante || {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}<br />acc. de sixte et quinte diminuée || {{Times New Roman|V<sup>+6</sup>}}<br />acc. de sixte sensible || {{Times New Roman|V<sup>+4</sup>}}<br />acc. de quarte sensible<br />acc. de triton |} {| class="wikitable" |+ Notation des principaux accords en jazz |- ! scope="col" | Accord ! scope="col" | Chiffrage ! scope="col" | Renversements |- ! scope="row" | Triade majeure | X | rowspan="3" | Les renversements se notent en mettant la basse après une barre de fraction, par exemple pour la triade de ''do'' majeur : * état fondamental : C ; * premier renversement : C/E ; * second renversement : C/G. |- ! scope="row" | Triade mineure | Xm, X– |- ! scope="row" | Septième | X<sup>7</sup> |} {{clear}} Dans le cas d'un accord de septième de dominante, le nom de l'accord change selon que l'on est en musique classique ou en jazz : en musique classique, on donne le nom de la tonalité alors qu'en jazz, on donne le nom de la fondamentale. Ainsi, l'accord appelé « septième de dominante de ''do'' majeur » en musique classique, est appelé « ''sol'' sept » (G<sup>7</sup>) en jazz : la dominante (degré {{Times New Roman|V}}, dominante) de la tonalité de ''do'' majeur est la note ''sol''. Comment appelle-t-on en musique classique l'accord appelé « ''do'' sept » (C<sup>7</sup>) en jazz ? Les tonalités dont le ''do'' est la dominante sont les tonalités de ''fa'' majeur (''si''♭ à la clef) et de ''fa'' mineur harmonique (''si''♭, ''mi''♭, ''la''♭ et ''ré''♭ à la clef et ''mi''♮ accidentel). Il s'agit donc de l'accord de septième de dominante des tonalités de ''fa'' majeur et ''fa'' mineur harmonique. {| class="wikitable" |+ Accords fréquents pour quelques la tonalités majeures ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Accord parfait<br />{{Times New Roman|I<sup>5</sup>}} ! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}} |- |''Do'' majeur || || C<br />''do-mi-sol'' || G7<br />''sol-si-ré-fa'' |- |''Sol'' majeur || ''fa''♯ || G<br />''sol-si-ré'' || D7<br />''ré-fa''♯''-la-do'' |- |''Ré'' majeur || ''fa''♯, ''do''♯ || D<br />''ré-fa''♯''-la'' || A7<br />''la-do''♯''-mi-sol'' |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A<br />''la-do''♯''-mi'' || E7<br />''mi-sol''♯''-si-ré'' |- | ''Fa'' majeur || ''si''♭ || F<br />''fa-la-do'' || C7<br />''do-mi-sol-si''♭ |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭<br />''si''♭''-ré-fa'' || F7<br />''fa-la-do-mi''♭ |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭<br />''mi''♭''-sol-si''♭ || B♭7<br />''si''♭''-ré-fa-la''♭ |} {| class="wikitable" |+ Accords fréquents pour quelques la tonalités mineures harmoniques ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Accord parfait<br />{{Times New Roman|i<sup>5</sup>}} ! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}} |- |''La'' mineur<br />harmonique || || Am, A–<br />''la-do-mi'' || E7<br />''mi-sol''♯''-si-ré'' |- |''Mi'' mineur<br />harmonique || ''fa''♯ || Em, E–<br />''mi-sol-si'' || B7<br />''si-ré''♯''-fa''♯''-la'' |- |''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || Bm, B–<br />''si-ré-fa''♯ || F♯7<br />''fa''♯''la''♯''-do''♯''-mi'' |- |''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || F♯m, F♯–<br />''fa''♯''-la-do''♯ || C♯7<br />''do''♯''-mi''♯''-sol''♯''-si'' |- | ''Ré'' mineur<br />harmonique || ''si''♭ || Dm, D–<br />''ré-fa-la'' || A7<br />''la-do''♯''-mi-sol'' |- | ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || Gm, G–<br />''sol-si''♭''-ré'' || D7<br />''ré-fa''♯''-la-do'' |- | ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || Cm, C–<br />''do-mi''♭''-sol'' || G7<br />''sol-si''♮''-ré-fa'' |} {{clear}} == Accords sur les degrés d'une gamme == === Harmonisation d'une gamme === [[Fichier:Accord trois notes gamme do majeur chiffre.svg|vignette|upright=1.2|Accords de trois note sur la gamme de ''do'' majeur, chiffrés.]] On peut ainsi construire une triade par degré d'une gamme. Pour une gamme majeure, les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|V<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|II<sup>5</sup>}}, {{Times New Roman|III<sup>5</sup>}}, {{Times New Roman|VI<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|ii<sup>5</sup>}}, {{Times New Roman|iii<sup>5</sup>}}, {{Times New Roman|vi<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}. Les accords ont tous une quinte juste à l'exception de l'accord {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} qui a une quinte diminuée, raison pour laquelle le « 5 » est barré. C'est un accord dit « de quinte diminuée ». En jazz, l'accord diminué est noté « dim », « ° », « m<sup>♭5</sup> » ou « <sup>–♭5</sup> ». Nous avons donc trois types d'accords (dans la notation jazz) : X (triade majeure), Xm (triade mineure) et X° (triade diminuée), la lettre X remplaçant le nom de la note fondamentale. {{clear}} [[Fichier:Accord trois notes gamme la mineur chiffre.svg|vignette|upright=1.2|Accords de trois notes sur une gamme de ''la'' mineur harmonique, chiffrés.]] Pour une gamme mineure harmonique, les accords {{Times New Roman|III<sup>+5</sup>}}, {{Times New Roman|V<sup>♯</sup>}} et {{Times New Roman|VI<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|II<sup><s>5</s></sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|i<sup>5</sup>}}, {{Times New Roman|ii<sup><s>5</s></sup>}}, {{Times New Roman|iv<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}. Les accords {{Times New Roman|ii<sup><s>5</s></sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} ont une quinte diminuée ; ce sont des accords dits « de quinte diminuée ». L'accord {{Times New Roman|III<sup>+5</sup>}} a une quinte augmentée ; le signe « plus » indique que la note de cinquième, le ''sol'' dièse, est la sensible. En jazz, l'accord est noté « aug » ou « <sup>+</sup> ». Les autres accords ont une quinte juste. Aux trois accords générés par une gamme majeure (X, Xm et X°), nous voyons ici apparaître un quatrième type d'accord : la triade augmentée X<sup>+</sup>. Nous remarquons que des gammes ont des accords communs. Par exemple, l'accord {{Times New Roman|ii<sup>5</sup>}} de ''do'' majeur est identique à l'accord {{Times New Roman|iv<sup>5</sup>}} de ''la'' mineur (il s'agit de l'accord Dm). Quel que soit le mode, les accords construits sur la sensible (accord de quinte diminuée) sont rarement utilisés. S'ils le sont, c'est en tant qu'accord de septième de dominante sans fondamentale (voir ci-après). C'est la raison pour laquelle le chiffrage indique le degré {{Times New Roman|V}} entre guillemets, et non pas le degré {{Times New Roman|VII}} (mais pour des raisons de clarté, nous l'indiquons entre parenthèses au début). En mode mineur, l'accord de quinte augmentée {{Times New Roman|iii<sup>+5</sup>}} est très peu utilisé (voir plus loin ''[[#Progression_d'accords|Progression d'accords]]''). C'est un accord considéré comme dissonant. On voit que : * un accord parfait majeur peut appartenir à cinq gammes différentes ;<br /> par exemple l'accord parfait de ''do'' majeur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> degré de la gamme de ''do'' majeur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' mineur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''mi'' mineur ; * un accord parfait mineur peut appartenir à cinq gammes différentes ;<br />par exemple l'accord parfait de ''la'' mineur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> de la gamme de ''la'' mineur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''mi'' mineur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|III}}<sup>e</sup> degré de ''fa'' majeur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''do'' majeur ; * un accord de quinte diminuée peut appartenir à trois gammes différentes ;<br />par exemple, l'accord de quinte diminuée de ''si'' est l'accord construit sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' majeur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''la'' mineur et sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' mineur ; * un accord de quinte augmentée (à l'état fondamental) ne peut appartenir qu'à une seule gamme ;<br /> par exemple, l'accord de quinte augmentée de ''do'' est l'accord construit sur le {{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur. {| class="wikitable" |+ Notation jazz des triades |- | rowspan="2" colspan="2" | ! scope="col" colspan="2" | Tierce |- ! scope="col" | 3m ! scope="col" | 3M |- ! rowspan="3" | Quinte ! scope="row" | 5d | Xᵒ, X<sub>m</sub><sup>(♭5)</sup> || |- ! scope="row" | 5J | Xm, X– || X |- ! scope="row" | 5A | || X+, X<sup>(♯5)</sup> |} === Harmonisation par des accords de septième === [[Fichier:Harmonisation gamme do majeur par septiemes chiffre.svg|vignette|upright=2|Harmonisation de la gamme de do majeur par des accords de septième.]] Les accords de septième contiennent une dissonance et créent ainsi une tension. Ils sont très utilisés en jazz. Nous avons représenté ci-contre l'harmonisation de la gamme de ''do'' majeur. La constitution des accords est la suivantes : * tierce majeure (3M) ** quinte juste (5J) *** septième mineure (7m) : sur le degré V, c'est l'accord de septième de dominante V<sup>7</sup><sub>+</sub>, noté X<sup>7</sup> (X pour G), *** septième majeure (7M) : sur les degrés I et IV, appelés « accords de septième majeure » et notés aussi X<sup>maj7</sup> ou X<sup>Δ</sup> (X pour C ou F) ; * tierce mineure (3m) ** quinte juste (5J) *** septième mineure : sur les degrés ii, iii et vi, appelés « accords mineur septième » et notés Xm<sup>7</sup> ou X–<sup>7</sup> (X pour D, E ou A), ** quinte diminuée (5d) *** septième mineure (7m) : sur le degré vii, appelé « accord demi-diminué » (puisque seule la quinte est diminuée) et noté X<sup>∅</sup> ou Xm<sup>7(♭5)</sup> ou X–<sup>7(♭5)</sup> (X pour B) ;<br /> en musique classique, on considère que c'est un accord de neuvième de dominante sans fondamentale. Nous avons donc quatre types d'accords : X<sup>7</sup>, X<sup>maj7</sup>, Xm<sup>7</sup> et X<sup>∅</sup> En jazz, on ajoute souvent la quarte à l'accord de sous-dominante IV (sur le ''fa'' dans une gamme de ''do'' majeur) ; il s'agit ici d'une quarte augmentée (''fa''-''si'') et l'accord est surnommé « accord lydien » mais cette dénomination est erronée (il s'agit d'une mauvaise interprétation de textes antiques). C'est un accord de onzième sans neuvième (la onzième étant l'octave de la quarte), il est noté X<sup>maj7(♯11)</sup> ou X<sup>Δ(♯11)</sup> (ici, F<sup>maj7(♯11)</sup>, ''fa''-''la''-''do''-''mi''-''si'' ou ''fa''-''la''-''si''-''do''-''mi''). {| class="wikitable" |+ Chiffrage jazz des accords de septième |- ! scope="col" rowspan="2" | Tierce ! scope="col" rowspan="2" | Quinte ! scope="col" colspan="2" | Septième |- ! scope="col" | 7m ! scope="col" | 7M |- | rowspan="2" | 3m | 5d || X<sup>∅</sup>, X<sub>m</sub><sup>7(♭5)</sup>, X–<sup>7(♭5)</sup> || |- | rowspan="2" | 5J | X<sub>m</sub><sup>7</sup>, X–<sup>7</sup> || |- | rowspan="2" | 3M | X<sup>7</sup> || X<sup>maj7</sup>, X<sup>Δ</sup> |- | 5A || || X<sub>+</sub><sup>maj7</sup>, X<sub>+</sub><sup>Δ</sup> |} === Modulation et emprunt === Un morceau peut comporter des changements de tonalité; appelés « modulation ». Il y a parfois un court passage dans une tonalité différente, typiquement sur une ou deux mesures, avant de retourner dans la tonalité d'origine : on parle d'emprunt. Lorsqu'il y a une modulation ou un emprunt, les degrés changent. Un même accord peut donc avoir une fonction dans une partie du morceau et une autre fonction ailleurs. L'utilisation d'accord différents, et en particulier d'accord utilisant des altérations accidentelles, indique clairement une modulation. Nous avons vu précédemment que les modulations courantes sont : * les modulations dans les tons voisins ; * les modulations homonymes ; * les marches harmoniques. Une modulation entre une tonalité majeure et mineure change la couleur du passage, * la modulation la plus « douce » est entre les tonalités relatives (par exemple''do'' majeur et ''la'' mineur) car ces tonalités utilisent quasiment les mêmes notes ; * la modulation la plus « voyante » est la modulation homonyme (par exemple entre ''do'' majeur et ''do'' mineur). Une modulation commence souvent sur l'accord de dominante de la nouvelle tonalité. Pour analyser un œuvre, ou pour improviser sur une partie, il est important de reconnaître les modulations. La description de la successind es tonalités s'appelle le « parcours tonal ». === Exercices élémentaires === L'apprentissage des accords passe par quelques exercices élémentaires. '''1. Lire un accord''' Il s'agit de lecture de notes : des notes composant les accords sont écrites « empilées » sur une portée, il faut les lire en énonçant les notes de bas en haut. '''2. Reconnaître la « couleur » d'un accord''' On écoute une triade et il faut dire si c'est une triade majeure ou mineure. Puis, on complexifie l'exercice en ajoutant la septième. '''3. Chiffrage un accord''' Trouver le nom d'un accord à partir des notes qui le composent. '''4. Réalisation d'un accord''' Trouver les notes qui composent un accord à partir de son nom. '''5. Dictée d'accords''' On écoute une succession d'accords et il faut soit écrire les notes sur une portée, soit écrire les noms de accords. [[File:Exercice constitution accord basse chiffree.svg|thumb|Exercice : constitution d'accord à partir de la basse chiffrée.]] '''Exercices de basse chiffrée''' ''Réalisation d'un accord'' Sur la figure suivante, écrire les notes des accords correspondant à la basse chiffrée. Déterminer le degré de la fondamentale pour chaque accord en considérant que nous sommes dans la tonalité de ''sol'' majeur. {{boîte déroulante/début|titre=Solution}} [[File:Exercice constitution accord basse chiffree solution.svg|vignette|Solution.]] # La note de basse est un ''do''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''mi'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''sol''.<br />Le chiffrage « <sup>5</sup> » indique que c'est un accord dans son état fondamental (l'écart entre deux notes consécutives ne dépasse pas la tierce), la fondamentale est donc la basse, ''do'', qui est le degré IV de la tonalité. # La note de basse est un ''si''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''ré'', puis nous appliquons le chiffrage 6 et ajoutons la sixte, ''sol''.<br />Le chiffrage « <sup>6</sup> » indique que c'est un accord dans son premier renversement. En le remettant dans son état fondamental, nous obtenons ''sol-si-ré'', la fondamentale est donc la tonique, le degré I. # La note de basse est un ''la''. Nous ajoutons la tierce (chiffre 3), ''do'', et la sixte (6), ''fa''♯. Nous vérifions que le ''fa''♯ est la sensible (signe +)<br />Nous voyons un « blanc » entre les notes ''do'' et ''fa''♯. En descendant le ''fa''♯ à l'octave inférieure, nous obtenons un empilement de tierces ''fa''♯-''la-do'', le fondamentale est donc ''fa''♯, le degré VII. Nous pouvons le voir comme le deuxième renversement de l'accord de septième de dominante, sans fondamentale. # La note de basse est un ''fa''♯. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''la'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''do'' ; nous vérifions qu'il s'agit bien d'une quinte diminuée (le 5 est barré). Nous appliquons le chiffre 6 et ajoutons la sixte, ''ré''.<br />Nous voyons que les notes ''do'' et ''ré'' sont conjointes (intervalle de seconde). En descendant le ''ré'' à l'octave inférieure, nous obtenons un empilement de tierces ''ré-fa''♯-''la-do'', le fondamentale est donc ''ré'', le degré V. Nous constatons que l'accord chiffré est le premier renversement de l'accord de septième de dominante. {{boîte déroulante/fin}} {{clear}} ''Chiffrage d'accords'' [[Fichier:Exercice chiffrage accord basse chiffree.svg|vignette|Accords à chiffrer.]] Chiffrer les accords ci-contre. {{boîte déroulante/début|titre=Solution}} [[File:Exercice chiffrage accord basse chiffree solution.svg|vignette|Solution.]] # On relève les intervalles en partant de la basse : tierce majeure (3M) et quinte juste (5J). Le chiffrage complet est donc ''fa''<sup>5</sup><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''fa''<sup>5</sup>.<br /> On peut aussi reconnaître que c'est l'accord parfait sur la tonique de la tonalité de ''fa'' majeur dans son état fondamental, le chiffrage d'un accord parfait étant <sup>5</sup>. # On relève les intervalles en partant de la basse : quarte juste (4J), sixte majeure (6M). Le chiffrage complet est donc ''fa''<sup>6</sup><sub>4</sub>.<br /> On peut aussi reconnaître que c'est le second renversement de l'accord ''mi-sol-si'', sur la tonique de la tonalité de ''mi'' mineur, le chiffrage du second renversement d'un accord parfait étant <sup>6</sup><sub>4</sub>. # Les intervalles en partant de la basse sont : tierce mineure (3m), quinte diminuée (5d), sixte mineure (6m). Le chiffrage complet est donc ''mi''<sup>6</sup><small><s>5</s></small><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''mi''<sup>6</sup><sub><s>5</s></sub>.<br /> On reconnaît le premier renversement de l'accord ''do-mi-sol-si''♭, accord de septième de dominante de la tonalité de ''fa'' majeur. # Les intervalles en partant de la basse sont : tierce mineure (3m), quinte juste (5J), septième mineure (7m). Le chiffrage complet est donc ''ré''<sup>7</sup><small>5</small><sub>3</sub> ; c'est typique d'un accord de septième de dominante, son chiffrage est donc ''ré''<sup>7</sup><sub>+</sub>.<br /> On reconnaît l'accord de septième de dominante de la tonalité de ''sol'' mineur dans son état fondamental. {{boîte déroulante/fin}} {{clear}} [[File:Exercice constitution accord notation jazz.svg|thumb|Exercice : constitution d'un accord d'après son chiffrage en notation jazz.]] '''Exercices de notation jazz''' ''Réalisation d'un accord'' Sur la figure suivante, écrire les notes des accords correspondant aux chiffrages. {{boîte déroulante/début|titre=Solution}} [[File:Exercice constitution accord notation jazz solution.svg|thumb|Solution.]] # Il s'agit de la triade majeure de ''do'' dans son état fondamental. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''do-mi-sol''. # Il s'agit de la triade majeure de ''sol''. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''sol-si-ré''. On renverse l'accord afin que la basse soit le ''si'', l'accord est donc ''si-ré-sol''. # Il s'agit de l'accord demi-diminué de ''fa''♯. Les intervalles sont la tierce mineure (3m), la quinte diminuée (5d) et la septième mineure (7m). Les notes sont donc ''fa''♯-''la-do-mi''. Nous renversons l'accord afin que la basse soit le ''la'', l'accord est donc ''a-do-mi-fa''♯. # Il s'agit de l'accord de septième de ''ré''. Les intervalles sont donc la tierce majeure (3M), la quinte juste (5J) et la septième mineure (7m). Les notes sont ''ré-fa''♯''-la-do''. Nous renversons l'accord afin que la basse soit le ''fa''♯, l'accord est donc ''fa''♯''-la-do-ré''. {{boîte déroulante/fin}} {{clear}} ''Chiffrage d'accords'' [[File:Exercice chiffrage accord notation jazz.svg|thumb|Accords à chiffrer.]] Chiffrer les accords ci-contre. {{boîte déroulante/début|titre=Solution}} [[File:Exercice chiffrage accord notation jazz solution.svg|thumb|Solution.]] # Les notes sont toutes sur des interlignes consécutifs, c'est donc un empilement de tierces ; l'accord est dans son état fondamental. Les intervalles sont une tierce majeure (''fa-la'' : 3M) et une quinte juste (''fa-do'' : 5J), c'est donc la triade majeure de ''fa''. Le chiffrage est F. # Il y a un blanc dans l'empilement des notes, c'est donc un accord renversé. En permutant les notes pour n'avoir que des tierces, on trouve l'accord ''mi-sol-si''. Les intervalles sont une tierce mineure (''mi-sol'' : 3m) et une quinte juste (''mi-si'' : 5J), c'est donc la triade mineure de ''mi'' avec un ''si'' à la basse. Le chiffrage est Em/B ou E–/B. # Il y deux notes conjointes, c'est donc un renversement. L'état fondamental de cet accord est ''do-mi-sol-si''♭. Les intervalles sont une tierce majeure (''do-mi'' : 3M), une quinte juste (''do-sol'' : 5J) et une septième mineure (''do-si''♭ : 7m). C'est donc l'accord de ''do'' septième avec un ''mi'' à la basse, chiffré C<sup>7</sup>/E. # Les notes sont toutes sur des interlignes consécutifs, l'accord est dans son état fondamental. Les intervalles sont la tierce mineure (''ré-fa'' : 3m), une quinte juste (''ré-la'' : 5J) et une septième mineure (''ré-do'' : 7m). C'est donc l'accord de ''ré'' mineur septième, chiffré Dm<sup>7</sup> ou D–<sup>7</sup>. {{boîte déroulante/fin}} {{clear}} == Harmonie fonctionnelle == Le choix des accords et de leur succession — la progression des accords — est un élément important d'un morceau, de sa composition. Le compositeur ou la compositrice a bien sûr une liberté totale, mais pour faire des choix, il faut comprendre les conséquences de ces choix, et donc ici, les effets produits par les accords et leur progression. Une des manières d'aborder le sujet est l'harmonie fonctionnelle. === Les trois fonctions des accords === En harmonie tonale, on considère que les accords ont une fonction. Il existe trois fonctions : * la fonction de tonique, {{Times New Roman|I}} ; * la fonction de sous-dominante, {{Times New Roman|IV}} ; * la fonction de dominante, {{Times New Roman|V}}. L'accord de tonique, {{Times New Roman|I}}, est l'accord « stable » de la tonalité par excellence. Il conclut en général les morceaux, et ouvre souvent les morceaux ; il revient fréquemment au cours du morceau. L'accord de dominante, {{Times New Roman|V}}, est un accord qui introduit une instabilité, une tension. En particulier, il contient la sensible (degré {{Times New Roman|VI}}), qui est une note « aspirée » vers la tonique. Cette tension, qui peut être renforcée par l'utilisation d'un accord de septième, est fréquemment résolue par un passage vers l'accord de tonique. Nous avons donc deux mouvements typiques : {{Times New Roman|I}} → {{Times New Roman|V}} (création d'une tension, d'une attente) et {{Times New Roman|V}} → {{Times New Roman|I}} (résolution d'une tension). Les accords de tonique et de dominante ont le cinquième degré en commun, cette note sert donc de pivot entre les deux accords. L'accord de sous-dominante, {{Times New Roman|IV}}, est un accord qui introduit lui aussi une tension, mais moins grande : il ne contient pas la sensible. Notons que s'il est une quarte au-dessus de la tonique, il est aussi une quinte en dessous d'elle ; il est symétrique de l'accord de dominante. Il a donc un rôle similaire à l'accord de dominante, mais atténué. L'accord de sous-dominante aspire soit vers l'accord de dominante, très proche, et l'on a alors une augmentation de la tension ; soit vers l'accord de tonique, un retour vers la stabilité (il a alors un rôle semblable à la dominante). Du fait de ces deux bifurcations possibles — augmentation de la tension ({{Times New Roman|IV}} → {{Times New Roman|V}}) ou retour à la stabilité ({{Times New Roman|IV}} → {{Times New Roman|I}}) —, l'utilisation de l'accord de sous-dominante introduit un certain flottement : si l'on peut facilement prédire l'accord qui suit un accord de dominante, on ne peut pas prédire ce qui suit un accord de sous-dominante. Notons que la composition ne consiste pas à suivre ces règles de manière stricte, ce qui conduirait à des morceaux stéréotypés et plats. Le plaisir d'écoute joue sur une alternance entre satisfaction d'une attente (respect des règles) et surprise (rompre les règles). === Accords remplissant ces fonctions === Les accords sur les autres degrés peuvent se ramener à une de ces trois fonctions : * {{Times New Roman|II}} : fonction de sous-dominante {{Times New Roman|IV}} ; * {{Times New Roman|III}} (très peu utilisé en mode mineur en raison de sa dissonance) et {{Times New Roman|VI}} : fonction de tonique {{Times New Roman|I}} ; * {{Times New Roman|VII}} : fonction de dominante {{Times New Roman|V}}. En effet, les accords étant des empilements de tierces, des accords situés à une tierce l'un de l'autre — {{Times New Roman|I}} ↔ {{Times New Roman|III}}, {{Times New Roman|II}} ↔ {{Times New Roman|IV}}, {{Times New Roman|V}} ↔ {{Times New Roman|VII}}, {{Times New Roman|VI}} ↔ {{Times New Roman|VIII}} ( = {{Times New Roman|I}}) — ont deux notes en commun. On retrouve le fait que l'accord sur le degré {{Times New Roman|VII}} est considéré comme un accord de dominante sans tonique. En mode mineur, l'accord sur le degré {{Times New Roman|III}} est évité, il n'a donc pas de fonction. {|class="wikitable" |+ Fonction des accords |- ! scope="col" | Fondamentale ! scope="col" | Fonction |- | {{Times New Roman|I}} || tonique |- | {{Times New Roman|II}} || sous-dominante faible |- | {{Times New Roman|III}} || tonique faible |- | {{Times New Roman|IV}} || sous-dominante |- | {{Times New Roman|V}} || dominante |- | {{Times New Roman|VI}} || tonique faible |- | {{Times New Roman|VII}} || dominante faible |} Par exemple en ''do'' majeur : * fonction de tonique : '''''do''<sup>5</sup> (C)''', ''mi''<sup>5</sup> (E–), ''la''<sup>5</sup> (A–) ; * fonction de sous-dominante : '''''fa''<sup>5</sup> (F)''', ''ré''<sup>5</sup> (D–) ; * fonction de dominante : '''''sol''<sup>5</sup> (G)''' ou ''sol''<sup>7</sup><sub>+</sub> (G<sup>7</sup>), ''si''<sup> <s>5</s></sup> (B<sup>o</sup>). En ''la'' mineur harmonique : * fonction de tonique : '''''la''<sup>5</sup> (A–)''', ''fa''<sup>5</sup> (F) [, rarement : ''do''<sup>+5</sup> (C<sup>+</sup>)] ; * fonction de sous-dominante : '''''ré''<sup>5</sup> (D–)''', ''si''<sup> <s>5</s></sup> (B<sup>o</sup>) ; * fonction de dominante : '''''mi''<sup>5</sup> (E)''' ou ''mi''<sup>7</sup><sub>+</sub> (E<sup>7</sup>), ''sol''♯<sup> <s>5</s></sup> (G♯<sup>o</sup>). Le fait d'utiliser des accords différents pour remplir une fonction permet d'enrichir l'harmonie, et de jouer sur l'équilibre entre satisfaction d'une attente (on respecte les règles sur les fonctions) et surprise (mais on n'utilise pas l'accord attendu). === Les dominantes secondaires === On utilise aussi des accords de septième dominante se fondant sur un autre degré que la dominante de la gamme ; on parle de « dominante secondaire ». Typiquement, avant un accord de septième de dominante, on utilise parfois un accord de dominante de dominante, dont le degré est alors noté « {{Times New Roman|V}} de {{Times New Roman|V}} » ou « {{Times New Roman|V}}/{{Times New Roman|V}} » ; la fondamentale est de l'accord est alors situé cinq degrés au-dessus de la dominante ({{Times New Roman|V}}), c'est donc le degré {{Times New Roman|IX}}, c'est-à-dire le degré {{Times New Roman|II}} de la tonalité en cours). Ou encore, on utilise un accord de dominante du degré {{Times New Roman|IV}} (« {{Times New Roman|V}} de {{Times New Roman|IV}} », la fondamentale est alors le degré {{Times New Roman|I}}) avant un accord sur le degré {{Times New Roman|IV}} lui-même. Par exemple, en tonalité de ''do'' majeur, on peut trouver un accord ''ré - fa''♯'' - la - do'' (chiffré {{Times New Roman|V}} de {{Times New Roman|V}}<sup>7</sup><sub>+</sub>), avant un accord ''sol - si - ré - fa'' ({{Times New Roman|V}}<sup>7</sup><sub>+</sub>). L'accord ''ré - fa''♯'' - la - do'' est l'accord de septième de dominante des tonalités de ''sol''. Dans la même tonalité, on pourra utiliser un accord ''do - mi - sol - si''♭ ({{Times New Roman|V}} de {{Times New Roman|IV}}<sup>7</sup><sub>+</sub>) avant un accord ''fa - la - do'' ({{Times New Roman|IV}}<sup>5</sup>). Le recours à une dominante secondaire peut atténuer une transition, par exemple avec un enchaînement ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> → ''fa''<sup>5</sup> (C → C<sup>7</sup> → F) qui correspond à un enchaînement {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}} : le passage ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> (C → C<sup>7</sup>) se fait en ajoutant une note (le ''si''♭) et rend naturel le passage ''do'' → ''fa''. Sur les sept degré de la gamme, on ne considère en général que cinq dominantes secondaires : en effet, la dominante du degré {{Times New Roman|I}} est la dominante « naturelle, primaire » de la tonalité (et n'est donc pas secondaire) ; et utiliser la dominante de {{Times New Roman|VII}} consisterait à considérer l'accord de {{Times New Roman|VII}} comme un accord propre, on évite donc les « {{Times New Roman|V}} de “{{Times New Roman|V}}” » (mais les « “{{Times New Roman|V}}” de {{Times New Roman|V}} » sont tout à fait « acceptables »). === Enchaînements classiques === Nous avons donc vu que l'on trouve fréquemment les enchaînements suivants : * pour créer une instabilité : ** {{Times New Roman|I}} → {{Times New Roman|V}}, ** {{Times New Roman|I}} → {{Times New Roman|IV}} (instabilité moins forte mais incertitude sur le sens d'évolution) ; * pour maintenir l'instabilité : ** {{Times New Roman|IV}} → {{Times New Roman|V}} ; * pour résoudre l'instabilité : ** {{Times New Roman|IV}} → {{Times New Roman|I}}, ** {{Times New Roman|V}} → {{Times New Roman|I}}, cas particuliers (voir plus bas) : *** {{Times New Roman|V}}<sup>+4</sup> → {{Times New Roman|I}}<sup>6</sup>, *** {{Times New Roman|I}}<sup>6</sup><sub>4</sub> → {{Times New Roman|V}}<sup>7</sup><sub>+</sub> → {{Times New Roman|I}}<sup>5</sup>. Les degrés indiqués ci-dessus sont les fonctions ; on peut donc utiliser les substitutions suivantes : * {{Times New Roman|I}} par {{Times New Roman|VI}} et, en tonalité majeure, {{Times New Roman|III}} ; * {{Times New Roman|IV}} par {{Times New Roman|II}} ; * {{Times New Roman|V}} par {{Times New Roman|VII}}. Pour enrichir l'harmonie, on peut utiliser les dominantes secondaires, en particulier : * {{Times New Roman|V}} de {{Times New Roman|V}} ({{Times New Roman|II}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|V}}, * {{Times New Roman|V}} de {{Times New Roman|IV}} ({{Times New Roman|I}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|IV}}. On peut enchaîner les enchaînements, par exemple {{Times New Roman|I}} → {{Times New Roman|IV}} → {{Times New Roman|V}}, ou encore {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}}… En jazz, on utilise très fréquemment l'enchaînement {{Times New Roman|II}} → {{Times New Roman|V}} → {{Times New Roman|I}} (deux-cinq-un). On peut bien sûr avoir d'autres enchaînements, mais ces règles permettent d'analyser un grand nombre de morceaux, et donnent des clefs utiles pour la composition. Nous voyons ci-après un certain nombre d'enchaînements courants dans différents styles == Exercice == Un hautboïste travaille la sonate en ''do'' mineur S. 277 de Heinichen. Sur le deuxième mouvement ''Allegro'', il a du mal à travailler un passage en raison des altérations accidentelles. Sur la suggestion de sa professeure, il décide d'analyser la progression d'accords sous-jacente afin que les altérations deviennent logiques. Il s'agit d'un duo hautbois et basson pour lequel les accords ne sont pas chiffrés, le basson étant ici un instrument soliste et non pas un élément de la basse continue. Sur l'extrait suivant, déterminez les basses et la qualité (chiffrage) des accords sous-jacents. Commentez. [[Fichier:Sonate hautbois basson heinichen 2e mvt mes49.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen.]] {{note|L'œuvre est en ''do'' mineur et devrait donc avoir trois bémols à la clef, or ici il n'y en a que deux. En effet, le ''la'' pouvant être bécarre en mode mineur mélodique ascendant, le compositeur a préféré le noter explicitement en altération accidentelle lorsque l'on est en mode mélodique naturel, harmonique ou mélodique descendant. C'est un procédé assez courant à l'époque baroque.}} {{boîte déroulante/début|titre=Solution}} Une des difficultés ici est que les arpèges joués par les instruments sont agrémentés de notes de passage. Les notes de la basse (du basson) sont différentes entre le premier et le deuxième temps de chaque mesure et ne peuvent pas appartenir au même accord. On a donc un accord par temps. Sur le premier temps de chaque mesure, le basson joue une octave. La note concernée est donc la basse de chaque accord. Pour savoir s'il s'agit d'un accord à l'état fondamental ou d'un renversement, on regarde ce que joue le hautbois : dans un mouvement conjoint (succession d'intervalles de secondes), il est difficile de distinguer les notes de l'arpège des notes de passage, mais : les notes des grands intervalles font partie de l'accord. Ainsi, sur le premier temps de la première mesure (la basse est un ''mi''♭), on a une sixte descendante ''sol''-''si''♭ et, à la fin du temps, une tierce descendante ''sol''-''mi''♭. L'accord est donc ''mi''♭-''sol''-''si''♭, c'est un accord de quinte (accord parfait à l'état fondamental). À la fin du premier temps, le basson joue un ''do'', c'est donc une note étrangère. Sur le second temps de la première mesure, le basson joue une tierce ascendante ''fa''-''la''♭, la première note est la basse de l'accord et la seconde une des notes de l'accord. Le hautbois commence par une sixte descendante ''la''♭-''do'', l'accord est donc ''fa''-''la''♭-''do'', un accord de quinte (accord parfait à l'état fondamental). Le ''do'' du basson la fin du premier temps est donc une anticipation. Les autres notes étrangères de la première mesure sont des notes de passage. Mais il faut faire attention : en suivant ce principe, sur les premiers temps des deuxième et troisième mesure, nous aurions des accords de septième d'espèce (puisque la septième est majeure). Or, on ne trouve pas, ou alors exceptionnellement, d'accord de septième d'espèce dans le baroque, mais quasi exclusivement des accords de septième de dominante. Donc au début de la deuxième mesure, le ''la''♮ est une appoggiature du ''si''♭, l'accord est donc ''si''♭-''ré''-''fa'', un asscord de quinte. De même, au début de la troisième mesure, le ''sol'' est une appoggiature du ''la''♭. Il faut donc se méfier d'une analyse purement « mathématique ». Il faut s'attacher à ressentir la musique, et à connaître les styles, pour faire une analyse pertinente. Ci-dessous, nous avons grisé les notes étrangères. [[Fichier:Sonate hautbois basson heinichen 2e mvt mes49 analyse.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen. Analyse de la progression harmonique.]] Le chiffrage jazz équivalent est : : | E♭ F– | B♭<sup>Δ</sup> E♭ | A♭<sup>Δ</sup> D– | G … Nous remarquons une progression assez régulière : : ''mi''♭ ↗[2<sup>de</sup>] ''fa'' | ↘[5<sup>te</sup>] ''si''♭ ↗[4<sup>te</sup>] ''mi''♭ | ↘[5<sup>te</sup>] ''la''♭ ↗[4<sup>te</sup>] ''ré'' | ↘[5<sup>te</sup>] ''sol'' Le ''mi''♭ est le degré {{Times New Roman|III}} de la tonalité principale (''do'' mineur), c'est donc une tonique faible ; il « joue le même rôle » qu'un ''do''. S'il y avait eu un accord de ''do'' au début de l'extrait, on aurait eu une progression parfaitement régulière ↗[4<sup>te</sup>] ↘[5<sup>te</sup>]. Nous avons les modulations suivantes : * mesure 49 : ''do'' mineur naturel (le ''si''♭ n'est pas une sensible) avec un accord sur “{{Times New Roman|I}}” (tonique faible, {{Times New Roman|III}}, pour la première analyse, ou bien tonique forte, {{Times New Roman|I}}, pour la seconde) suivi d'un accord sur {{Times New Roman|IV}} ; * mesure 50 : ''si''♭ majeur avec un accord sur {{Times New Roman|I}} suivi d'un accord sur {{Times New Roman|IV}} ; * mesure 51 : ''la''♭ majeur avec un accord sur {{Times New Roman|I}}, et emprunt à ''do'' majeur avec un accord sur {{Times New Roman|II}} ({{Times New Roman|IV}} faible). On a donc une marche harmonique {{Times New Roman|I}} → {{Times New Roman|IV}} qui descend d'une seconde majeure (un ton) à chaque mesure (''do'' → ''si''♭ → ''la''♭), avec une exception sur la dernière mesure (modulation en cours de mesure et descente d'une seconde mineure au lieu de majeure). Ce passage est donc construit sur une régularité, une règle qui crée un effet d'attente — enchaînement {{Times New Roman|I}}<sup>5</sup> → {{Times New Roman|IV}}<sup>5</sup> avec une marche harmonique d'une seconde majeure descendante —, et des « surprises », des exceptions au début — ce n'est pas un accord {{Times New Roman|I}}<sup>5</sup> mais un accord {{Times New Roman|III}}<sup>5</sup> — et à la fin — modulation en milieu de mesure et dernière descente d'une seconde mineure (½t ''la''♭ → ''sol''). L'extrait ne permet pas de le deviner, mais la mesure 52 est un retour en ''do'' mineur, avec donc une modulation sur la dominante (accord de ''sol''<sup>7</sup><sub>+</sub>, G<sup>7</sup>). {{boîte déroulante/fin}} == Progression d'accords == Comme pour la mélodie, la succession des accords dans un morceau, la progression d'accords, suit des règles. Et comme pour la mélodie, les règles diffèrent d'un style musical à l'autre et la créativité consiste à parfois ne pas suivre ces règles. Et comme pour la mélodie, on part d'un ensemble de notes organisé, d'une gamme caractéristique d'une tonalité, d'un mode. Les accords les plus utilisés pour une tonalité donnée sont les accords dont la fondamentale sont les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la tonalité, en particulier la triade {{Times New Roman|I}}, appelée « accord parfait » ou « accord de tonique », et l'accord de septième {{Times New Roman|V}}, appelé « septième de dominante ». Le fait d'avoir une progression d'accords qui se répète permet de structurer un morceau. Pour les morceaux courts, il participe au plaisir de l'écoute et facilite la mémorisation (par exemple le découpage couplet-refrain d'une chanson). Sur les morceaux longs, une trop grande régularité peut introduire de la lassitude, les longs morceaux sont souvent découpés en parties présentant chacune une progression régulière. Le fait d'avoir une progression régulière permet la pratique de l'improvisation : cadence en musique classique, solo en jazz et blues. ; Note : Le terme « cadence » désigne plusieurs choses différentes, et notamment en harmonie : :* une partie improvisée dans un opéra ou un concerto, sens utilisé ci-dessus ; :* une progression d'accords pour ponctuer un morceau et en particulier pour le conclure, sens utilisé dans la section suivante. === Accords peu utilisés === En mode mineur, l'accord de quinte augmentée {{Times New Roman|III<sup>+5</sup>}} est très peu utilisé. C'est un accord dissonant ; il intervient en général comme appogiature de l'accord de tonique (par exemple en ''la'' mineur : {{Times New Roman|III<sup>+5</sup>}} ''do'' - ''mi'' - ''sol''♯ → {{Times New Roman|I<sup>6</sup>}} ''do'' - ''mi'' - ''la''), ou de l'accord de dominante ({{Times New Roman|III<sup>6</sup><sub>+3</sub>}} ''mi'' - ''sol''♯ - ''do'' → {{Times New Roman|V<sup>5</sup>}} ''mi'' - ''sol''♯ - ''si''). Il peut être aussi utilisé comme préparation à l'accord de sous-dominante (enchaînement {{Times New Roman|III}} → {{Times New Roman|IV}}). Par ailleurs, il a une constitution symétrique — c'est l'empilement de deux tierces majeures — et ses renversements ont les mêmes intervalles à l'enharmonie près (quinte augmentée/sixte mineure, tierce majeure/quarte diminuée). De ce fait, un même accord est commun, par renversement et à l'enharmonie près, à trois tonalités : le premier renversement de l'accord ''do'' - ''mi'' - ''sol''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur) est enharmonique à ''mi'' - ''sol''♯ - ''si''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''do''♯ mineur) ; le second renversement est enharmonique à ''la''♭ - ''do'' - ''mi'' ({{Times New Roman|III}}<sup>e</sup> degré de ''fa'' mineur). === Accords très utilisés === Les trois accords les plus utilisés sont les accords de tonique (degré {{Times New Roman|I}}), de sous-dominante ({{Times New Roman|IV}}) et de dominante ({{Times New Roman|V}}). Ils interviennent en particulier en fin de phrase, dans les cadences. L'accord de dominante sert souvent à introduire une modulation : la modulation commence sur l'accord de dominante de la nouvelle tonalité. On note que l'accord de sous-dominante est situé une quinte juste en dessous de la tonique, les accords de dominante et de sous-dominante sont donc symétriques. En jazz, on utilise également très fréquemment l'accord de la sus-tonique (degré {{Times New Roman|II}}), souvent dans des progressions {{Times New Roman|II}} - {{Times New Roman|V}} (- {{Times New Roman|I}}). Rappelons que l'accord de sus-tonique a la fonction de sous-dominante. === Cadences et ''turnaround'' === Le terme « cadence » provient de l'italien ''cadenza'' et désigne la « chute », la fin d'un morceau ou d'une phrase musicale. On distingue deux types de cadences : * les cadences conclusive, qui créent une sensation de complétude ; * les cadences suspensives, qui crèent une sensation d'attente. ==== Cadence parfaite ==== [[Fichier:Au clair de le lune cadence parfaite.midi|thumb|''Au clair de la lune'', harmonisé avec une cadence parfaite (italienne).]] [[Fichier:Au clair de le lune mineur cadence parfaite.midi|thumb|''Idem'' mais en mode mineur harmonique.]] La cadence parfaite est l'enchaînement de l'accord de dominante suivi de l'accord parfait : {{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}}, les deux accord étant à l'état fondamental. Elle donne une impression de stabilité et est donc très souvent utilisée pour conclure un morceau. C'est une cadence conclusive. On peut aussi utiliser l'accord de septième de dominante, la dissonance introduisant une tension résolue par l'accord parfait : {{Times New Roman|V<sup>7</sup><sub>+</sub> - I<sup>5</sup>}}. Elle est souvent précédée de l'accord construit sur le IV<sup>e</sup> degré, appelé « accord de préparation », pour former la cadence italienne : {{Times New Roman|IV<sup>5</sup> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}}. Elle est également souvent précédée du second renversement de l'accord de tonique, qui est alors appelé « appoggiature de la cadence » : {{Times New Roman|I<sup>6</sup><sub>4</sub> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}} (on remarque que les accords {{Times New Roman|I}}<sup>6</sup><sub>4</sub> et {{Times New Roman|V}}<sup>5</sup> ont la basse en commun, et que l'on peut passer de l'un à l'autre par un mouvement conjoint sur les autres notes). {{clear}} ==== Demi-cadence ==== [[Fichier:Au clair de le lune demi cadence.midi|thumb|''Au clair de la lune'', harmonisé avec une demi-cadence.]] Une demi-cadence est une phrase ou un morceau se concluant sur l'accord construit sur le cinquième degré. Il provoque une sensation d'attente, de suspens. Il s'agit en général d'une succession {{Times New Roman|II - V}} ou {{Times New Roman|IV - V}}. C'est une cadence suspensive. On utilise rarement un accord de septième de dominante. {{clear}} ==== Cadence rompue ou évitée ==== La cadence rompue, ou cadence évitée, est succession d'un accord de dominante et d'un accord de sus-dominante, {{Times New Roman|V}} - {{Times New Roman|VI}}. C'est une cadence suspensive. ==== Cadence imparfaite ==== Une cadence imparfaite est une cadence {{Times New Roman|V - I}}, comme la cadence parfaite, mais dont au moins un des deux accords est dans un état renversé. ==== Cadence plagale ==== La cadence plagale — du grec ''plagios'', oblique, en biais — est la succession de l'accord construit sur le quatrième degré, suivi de l'accord parfait : {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}. Elle peut être utilisée après une cadence parfaite ({{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}} - {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}). Elle donne un caractère solennel, voire religieux — elle est parfois appelée « cadence amen » —, elle a un côté antique qui rappelle la musique modale et médiévale<ref>{{lien web |url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/la-cadence-amen-ou-comment-se-dire-adieu-7191921 |titre=La cadence « Amen » ou comment se dire adieu |auteur=Max Dozolme (MAXXI Classique) |site=France Musique |date=2025-04-25 |consulté le=2025-04-25}}.</ref>. C'est une cadence conclusive. ==== {{lang|en|Turnaround}} ==== [[Fichier:Au clair de le lune turnaround.midi|thumb|Au clair de la lune, harmonisé en style jazz : accords de 7{{e}}, anatole suivie d'un ''{{lang|en|turnaround}}'' ii-V-I.]] Le terme ''{{lang|en|turnaround}}'' signifie revirement, retournement. C'est une succession d'accords que fait la transition entre deux parties, en créant une tension-résolution. Le ''{{lang|en|turnaround}}'' le plus courant est la succession {{Times New Roman|II - V - I}}. On utilise également fréquemment l'anatole : {{Times New Roman|I - VI - II - V}}. {| class="wikitable" |+ Progressions typiques d'accords dans une tonalité majeure ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Cadence<br />parfaite<br /> {{Times New Roman|V - I}} ! scope="col" | Cadence<br />italienne<br />{{Times New Roman|IV - V - I}} ! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou IV - V}} ! scope="col" | Cadence<br />plagale<br />{{Times New Roman|IV - I}} ! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}} ! scope="col" | Anatole<br />{{Times New Roman|I - vi - ii - V}} |- |''Do'' majeur || || G - C || F - G - C || Dm - G ou F - G || F - C || Dm - G - C || C - Am - Dm - G |- |''Sol'' majeur || ''fa''♯ || D - G || C - D - G || Am - D ou C - D || C - G || Am - D - G || G - Em - Am - D |- |''Ré'' majeur || ''fa''♯, ''do''♯ || A - D || G - A - D || Em - A ou G - A || G - D || Em - A - D || D - Bm - Em - A |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || E - A || D - E - A || Bm - E ou D - E || D - A || Bm - E - A || A - F♯m - B - E |- | ''Fa'' majeur || ''si''♭ || C - F || B♭ - C - F || Gm - C ou B♭ - C || B♭ - F || Gm - C - F || F - Dm - Gm - C |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || F - B♭ || E♭ - F - B♭ || Cm - F ou E♭ - F || E♭ - B♭ || Cm - F - B♭ || B♭ - Gm - Cm - F |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || B♭ - E♭ || A♭ - B♭ - E♭ || Fm - B♭ ou A♭ - B♭ || A♭ - E♭ || Fm - B♭ - E♭ || Gm - Cm - Fm - B♭ |} {| class="wikitable" |+ Progressions typiques d'accords dans une tonalité mineure ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Cadence<br />parfaite<br />{{Times New Roman|V - i}} ! scope="col" | Cadence<br />italienne<br />{{Times New Roman|iv - V - i}} ! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou iv - V}} ! scope="col" | Cadence<br />plagale<br />{{Times New Roman|iv - i}} ! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}} ! scope="col" | Anatole<br />{{Times New Roman|i - VI - ii - V}} |- | ''La'' mineur<br />harmonique || || E - Am || Dm - E - Am || B° - E ou Dm - E || Dm - Am || B° - E - Am || Am - F - B° - E |- | ''Mi'' mineur<br />harmonique || ''fa''♯ || B - Em || Am - B - Em || F♯° - B ou Am - B || Am - Em || F♯° - B - Em || Em - C - F♯° - B |- | ''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || F♯ - Bm || Em - F♯ - Bm || C♯° - F♯ ou Em - F♯ || Em - Bm || C♯° - F♯ - Bm || Bm - G - C♯° - F♯ |- | ''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || C♯ - F♯m || Bm - C♯ - F♯m || G♯° - C♯ ou Bm - C♯ || Bm - F♯m || G♯° - C♯ - F♯m || A+ - D - G♯° - C♯ |- | ''Ré'' mineur<br />harmonique || ''si''♭ || A - Dm || Gm - A - Dm || E° - A ou Gm - A || Gm - Dm || E° - A - Dm || Dm - B♭ - E° - A |- | ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || D - Gm || Cm - D - Gm || A° - D ou Cm - D || Cm - Gm|| A° - D - Gm || Gm - E♭ - A° - D |- | ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || G - Cm || Fm - G - Cm || D° - G ou Fm - G || Fm - Dm || D° - G - Cm || Cm - A♭ - D° - G |} ==== Exemple : ''La Mer'' ==== : {{lien web | url = https://www.youtube.com/watch?v=PXQh9jTwwoA | titre = Charles Trenet - La mer (Officiel) [Live Version] | site = YouTube | auteur = Charles Trenet | consulté le = 2020-12-24 }} Le début de ''La Mer'' (Charles Trenet, 1946) est en ''do'' majeur et est harmonisé par l'anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (C - Am - Dm - G<sup>7</sup>) sur deux mesures, jouée deux fois ({{Times New Roman|1=<nowiki>|I-vi|ii-V</nowiki><sup>7</sup><nowiki>|</nowiki>}} × 2). Viennent des variations avec les progressions {{Times New Roman|I-III-vi-V<sup>7</sup>}} (C - E - Am - G<sup>7</sup>) puis la « progression ’50s » (voir plus bas) {{Times New Roman|I-vi-IV-VI<sup>7</sup>}} (C - Am - F - A<sup>7</sup>, on remarque que {{Times New Roman|IV}}/F est le relatif majeur du {{Times New Roman|ii}}/Dm de l'anatole), jouées chacune une fois sur deux mesure ; puis cette première partie se conclut par une demie cadence {{Times New Roman|ii-V<sup>7</sup>}} sur une mesure puis une dernière anatole sur trois mesures ({{Times New Roman|1=<nowiki>|I-vi|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}}). Cela constitue une première partie « A » sur douze mesures qui se termine par une demi-cadence ({{Times New Roman|ii-V<sup>7</sup>}}) qui appelle une suite. Cette partie A est jouée une deuxième fois mais la fin est modifiée pour la transition : les deux dernières mesures {{Times New Roman|<nowiki>|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}} deviennent {{Times New Roman|<nowiki>|ii-V</nowiki><sup>7</sup><nowiki>|I|</nowiki>}} (|Dm-G7|C|), cette partie « A’ » se conclut donc par une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}). Le morceau passe ensuite en tonalité de ''mi'' majeur, donc une tierce au dessus de ''do'' majeur, sur six mesures. Cette partie utilise une progression ’50s {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (E - C♯m - A - B<sup>7</sup>), qui est rappelons-le une variation de l'anatole, l'accord {{Times New Roman|ii}} (Fm) étant remplacé par son relatif majeur {{Times New Roman|IV}} (A). Cette anatole modifiée est jouée deux fois puis la partie en ''mi'' majeur se conclut par l'accord parfait {{Times New Roman|I}} joué sur deux mesures (|E|E|), on a donc, avec la mesure précédente, avec une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}). Suivent ensuite six mesures en ''sol'' majeur, donc à nouveau une tierce au dessus de ''mi'' majeur. Elle comporte une progression {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (G - Em - C - D<sup>7</sup>), donc anatole avec substitution du {{Times New Roman|ii}}/Am par son relatif majeur {{Times New Roman|VI}}/C (progression ’50s), puis une anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (G - Em - Am - D<sup>7</sup>) et deux mesure sur la tonique {{Times New Roman|I-I<sup>7</sup>}} (G - G<sup>7</sup>), formant à nouveau une cadence parfaite. La fin sur un accord de septième, dissonant, appelle une suite. Cette partie « B » de douze mesures comporte donc deux parties similaires « B1 » et « B2 » qui forment une marche harmonique (montée d'une tierce). Le morceau se conclut par une reprise de la partie « A’ » et se termine donc par une cadence parfaite. Nous avons une structure A-A’-B-A’ sur 48 mesures, proche la forme AABA étudiée plus loin. Donc ''La Mer'' est un morceau structuré autour de l'anatole avec des variations (progression ’50s, substitution du {{Times New Roman|ii}} par son relatif majeur {{Times New Roman|IV}}) et comportant une marche harmonique dans sa troisième partie. Les parties se concluent par des ''{{lang|en|turnarounds}}'' sous la forme d'une cadence parfaite ou, pour la partie A, par une demi-cadence. {| border="1" rules="rows" frame="hsides" |+ Structure de ''La Mer'' |- align="center" | | colspan="12" | ''do'' majeur | |- align="center" ! scope="row" rowspan=2 | A | colspan="2" | anatole | colspan="2" | // | colspan="2" | variation | colspan="2" | ’50s | ½ c. | colspan="3" | anatole | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii}} || <nowiki>|</nowiki> {{Times New Roman|V<sup>7</sup>}} || <nowiki>|</nowiki> |- align="center" ! scope="row" rowspan="2" | A’ | colspan="2" | anatole | colspan="2" | // | colspan="2" | variation | colspan="2" | ’50s | ½ c. | colspan="2" | anatole | c.p. | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> |- align="center" | | colspan="6" | B1 : ''mi'' majeur | colspan="6" background="lightgray" | B2 : ''sol'' majeur | |- align="center" ! scope="row" rowspan="2" | B | colspan="2" | ’50s | colspan="2" | // |colspan="2" | c.p. | colspan="2" | ’50s | colspan="2" | // |colspan="2" | c.p. | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I<sup>7</sup>}} || <nowiki>|</nowiki> |- ! scope="row" | A’ | colspan="12" | | |} === Progression blues === La musique blues est apparue dans les années 1860. Elle est en général bâtie sur une grille d'accords ''({{lang|en|changes}})'' immuable de douze mesures ''({{lang|en|twelve-bar blues}})''. C'est sur cet accompagnement qui se répète que s'ajoute la mélodie — chant et solo. Cette structure est typique du blues et se retrouve dans ses dérivés comme le rock 'n' roll. Le rythme est toujours un rythme ternaire syncopé ''({{lang|en|shuffle, swing, groove}}, ''notes inégales'')'' : la mesure est à quatre temps, mais la noire est divisée en noire-croche en triolet, ou encore triolet de croche en appuyant la première et la troisième. La mélodie se construit en général sur une gamme blues de six degrés (gamme pentatonique mineure avec une quarte augmentée), mais bien que la gamme soit mineure, l'harmonie est construite sur la gamme majeure homonyme : un blues en ''fa'' a une mélodie sur la gamme de ''fa'' mineur, mais une harmonie sur la gamme de ''fa'' majeur. La grille d'accord comporte les accords construits sur les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la gamme majeure homonyme. Les accords sont souvent des accords de septième (donc avec une tierce majeure et une septième mineure), il ne s'agit donc pas d'une harmonisation de gamme diatonique (puisque la septième est majeure sur l'accord de tonique). Par exemple, pour un blues en ''do'' : * accord parfait de do majeur, C ({{Times New Roman|I}}<sup>er</sup> degré) ; * accord parfait de fa majeur, F ({{Times New Roman|IV}}<sup>e</sup> degré) ; * accord parfait de sol majeur, G ({{Times New Roman|V}}<sup>e</sup> degré). Il existe quelques morceaux harmonisés avec des accords mineurs, comme par exemple ''As the Years Go Passing By'' d'Albert King (Duje Records, 1959). La progression blues est organisée en trois blocs de quatre mesures ayant les fonctions suivantes (voir ci-dessus ''[[#Harmonie fonctionnelle|Harmonie fonctionnelle]]'') : * quatre mesures toniques ; * quatre mesures sous-dominantes ; * quatre mesures dominantes. La forme la plus simple, que Jeff Gardner appelle « forme A », est la suivante : {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, forme A |- ! scope="row" | Tonique | width="50px" | I | width="50px" | I | width="50px" | I | width="50px" | I |- ! scope="row" | Sous-domminante | width="50px" | IV | width="50px" | IV | width="50px" | I | width="50px" | I |- ! scope="row" | Dominante | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | V |} La progression {{Times New Roman|I-V}} des deux dernières mesures forment le ''{{lang|en|turnaround}}'', la demie cadence qui lance le cycle suivant. Nous présentons ci-dessous un exemple typique de ligne de basse ''({{lang|en|walking bass}})'' pour le ''{{lang|en|turnaround}}'' d'un blues en ''la'' : [[Fichier:Turnaround classique blues en la.svg|Exemple typique de ligne de basse pour un ''turnaround'' de blues en ''la''.]] [[Fichier:Blues mi harmonie elementaire.midi|thumb|Blues en ''mi'', harmonisé de manière élémentaire avec une ''{{lang|en|walking bass}}''.]] Vous pouvez écouter ci-contre une harmonisation typique d'un blues en ''mi''. Les accords sont exécutés par une basse marchante ''({{lang|en|walking bass}})'', qui joue une arpège sur la triade avec l'ajout d'une sixte majeure et d'une septième mineure, et par une guitare qui joue un accord de puissance ''({{lang|en|power chord}})'', qui n'est composé que de la fondamentale et de la quinte juste, avec une sixte en appoggiature. La forme B s'obtient en changeant la deuxième mesure : on joue un degré {{Times New Roman|IV}} au lieu d'un degré {{Times New Roman|I}}. La progression {{Times New Roman|I-IV}} sur les deux premières mesures est appelé ''{{lang|en|quick change}}''. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, forme B |- | width="50px" | I | width="50px" | IV | width="50px" | I | width="50px" | I |- | width="50px" | IV | width="50px" | IV | width="50px" | I | width="50px" | I |- | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | V |} Par exemple, ''Sweet Home Chicago'' (Robert Johnson, 1936) est un blues en ''fa'' ; sa grille d'accords, aux variations près, suit une forme B : {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression de ''Sweet Home Chicago'' |- | width="50px" | F | width="50px" | B♭ | width="50px" | F | width="50px" | F |- | width="50px" | B♭ | width="50px" | B♭ | width="50px" | F | width="50px" | F |- | width="50px" | C7 | width="50px" | B♭7 | width="50px" | F7 | width="50px" | C7 |} : Écouter {{lien web | url =https://www.youtube.com/watch?v=dkftesK2dck | titre = Robert Johnson "Sweet Home Chicago" | auteur = Michal Angel | site = YouTube | date = 2007-12-09 | consulté le = 2020-12-17 }} Les formes C et D s'obtiennent à partir des formes A et B en changeant le dernier accord par un accord sur le degré {{Times New Roman|I}}, ce qui forme une cadence plagale. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, formes C et D |- | colspan="4" | … |- | colspan="4" | … |- | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | I |} L'harmonie peut être enrichie, notamment en jazz. Voici par exemple une grille du blues souvent utilisés en bebop. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Exemple de progression de blues bebop sur une base de forme B |- | width="60px" | I<sup>7</sup> | width="60px" | IV<sup>7</sup> | width="60px" | I<sup>7</sup> | width="60px" | V–<sup>7</sup> <nowiki>|</nowiki> I<sup>7</sup> |- | width="60px" | IV<sup>7</sup> | width="60px" | IV<sup>7</sup> | width="60px" | I<sup>7</sup> | width="60px" | VI<sup>7 ♯9 ♭13</sup> |- | width="60px" | II–<sup>7</sup> | width="60px" | V<sup>7</sup> | width="60px" | V<sup>7</sup> <nowiki>|</nowiki> IV<sup>7</sup> | width="60px" | II–<sup>7</sup> <nowiki>|</nowiki> V<sup>7</sup> |} On peut aussi trouver des blues sur huit mesures, sur seize mesures comme ''Watermelon Man'' de Herbie Hancock (album ''Takin' Off'', Blue Note, 1962) ou ''Let's Dance'' de Jim Lee (interprété par Chris Montez, Monogram, 1962) * {{lien web |url= https://www.dailymotion.com/video/x5iduwo |titre=Herbie Hancock - Watermelon Man (1962) |auteur=theUnforgettablesTv |site=Dailymotion |date=2003 |consulté le=2021-02-09 }} * {{lien web |url=https://www.youtube.com/watch?v=6JXshurYONc |titre=Let's Dance |auteur=Chris Montez |site=YouTube |date=2016-08-06 |consulté le=2021-02-09 }} À l'inverse, certains blues peuvent avoir une structure plus simple que les douze mesure ; par exemple ''Hoochie Coochie Man'' de Willie Dixon (interprété par Muddy Waters sous le titre ''Mannish Boy'', Chicago Blues, 1954) est construit sur un seul accord répété tout le long de la chanson. * {{lien web |url=https://www.dailymotion.com/video/x5iduwo |titre=Muddy Waters - Hoochie Coochie Man |auteur=Muddy Waters |site=Dailymotion |date=2012 | consulté le=2021-02-09 }} === Cadence andalouse === La cadence andalouse est une progression de quatre accords, descendant par mouvement conjoint : * en mode de ''mi'' (mode phrygien) : {{Times New Roman|IV}} - {{Times New Roman|III}} - {{Times New Roman|II}} - {{Times New Roman|I}} ;<br />par exemple en ''mi'' phrygien : Am - G - F - E ; en ''do'' phrygien : Fm - E♭ - D♭ - C ;<br />on notera que le degré {{Times New Roman|III}} est diésé dans l'accord final (ou bécarre s'il est bémol dans la tonalité) ; * en mode mineur : {{Times New Roman|I}} - {{Times New Roman|VII}} - {{Times New Roman|VI}} - {{Times New Roman|V}} ;<br />par exemple en ''la'' mineur : Am - G - F - E ; en ''do'' mineur : Cm - B♭ - A♭ - m ;<br />comme précédemment, on notera que le degré {{Times New Roman|VII}} est diésé dans l'accord final. === Progressions selon le cercle des quintes === [[Fichier:Cercle quintes degres tonalite majeure.svg|vignette|Cercle des quinte justes (parcouru dans le sens des aiguilles d'une montre) des degrés d'une tonalité majeure.]] La progression {{Times New Roman|V-I}} est la cadence parfaite, mais on peut aussi l'employer au milieu d'un morceau. Cette progression étant courte, sa répétition crée de la lassitude ; on peut la compléter par d'autres accords séparés d'une quinte juste, en suivant le « cercle des quintes » : {{Times New Roman|I-V-IX}}, la neuvième étant enharmonique de la seconde, on obtient {{Times New Roman|I-V-II}} ; en retournant la progression, on obtient le turnaround {{Times New Roman|II-V-I}}. En jazz, dans le morceau ''Topsy'' (Edgar Battle et Eddir Durham), la partie <span style="border-color: black; border-style: solid; border-width: thin;">C</span> est composée d'une progression en quintes descendantes d'accords de septième de dominante : D<sup>7</sup> - G<sup>7</sup> - C<sup>7</sup> - F<sup>7</sup> - B♭<sup>7</sup> ; les improvisatiosn sur cette partie peuvent donc se faire en modulant en ''sol'' (majeur ou mineur) - ''do'' (M/m) - ''fa'' (M/m) - ''si''♭ (M/m) - ''mi''♭ (M/m). On peut continuer de décrire le cercle des quintes : {{Times New Roman|I-V-II-VI}}, on obtient l'anatole dans le désordre ; on peut à l'inverse étendre les quintes vers la gauche, {{Times New Roman|IV-I-V-II-VI}}. En musique populaire, on trouve fréquemment une progression fondée sur les accord {{Times New Roman|I}}, {{Times New Roman|IV}}, {{Times New Roman|V}} et {{Times New Roman|VI}}, popularisée dans les années 1950. La « progression années 1950 », « progression ''{{lang|en|fifties ('50)}}'' » ''({{lang|en|'50s progression}})'' est dans l'ordre {{Times New Roman|I-VI-IV-V}}. On trouve aussi cette progression en musique classique. Si la tonalité est majeure, la triade sur la sus-dominante est mineure, les autres sont majeures, on notera donc souvent {{Times New Roman|I-vi-IV-V}}. On peut avoir des permutations circulaires (le dernier accord venant au début, ou vice-versa) : {{Times New Roman|vi-IV-V-I}}, {{Times New Roman|IV-V-I-vi}} et {{Times New Roman|V-I-vi-IV}}. {| class="wikitable" |+ Accords selon la tonalité ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" style="font-family:Times New Roman" | I ! scope="col" style="font-family:Times New Roman" | IV ! scope="col" style="font-family:Times New Roman" | V ! scope="col" style="font-family:Times New Roman" | vi |- |''Do'' majeur || || C || F || G || Am |- |''Sol'' majeur || ''fa''♯ || G || C || D || Em |- |''Ré'' majeur || ''fa''♯, ''do''♯ || D || G || A || Bm |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A || D || E || F♯m |- | ''Fa'' majeur || ''si''♭ || F || B♭ || C || Dm |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭ || E♭ || F || Gm |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭ || A♭ || B♭ || Cm |} Par exemple, en tonalité de ''do'' majeur, la progression {{Times New Roman|I-vi-IV-V}} sera C-Am-F-G. Il existe d'autres progressions utilisant ces accords mais dans un autre ordre, typiquement {{Times New Roman|I–IV–vi–V}} ou une de ses permutations circulaires : {{Times New Roman|IV–vi–V-I}}, {{Times New Roman|vi–V-I-IV}} ou {{Times New Roman|V-I-IV-vi}}. Ou dans un autre ordre. PV Nova l'illustre dans plusieurs de ses « expériences » dans la version {{Times New Roman|vi-V-IV-I}}, soit Am-G-F-C, ou encore {{Times New Roman|vi-IV-I-V}}, soit Am-F-C-G : : {{lien web | url = https://www.youtube.com/watch?v=w08LeZGbXq4 | titre = Expérience n<sup>o</sup> 6 — La Happy Pop | auteur = PV Nova | site = YouTube | date = 2011-08-20 | consulté le = 2020-12-13 }} et cela devient un gag récurrent avec son « chapeau des accords magiques qu'on nous ressort à toutes les sauces » : {{lien web | url = https://www.youtube.com/watch?v=VMY_vc4nZAU | titre = Expérience n<sup>o</sup> 14 — La Soupe dou Brasil | auteur = PV Nova | site = YouTube | date = 2012-10-03 | consulté le = 2020-12-17 }} Cette récurrence est également parodiée par le groupe The Axis of Awesome avec ses « chansons à quatre accords » ''({{lang|en|four-chords song}})'', dans une sketch où ils mêlent 47 chansons en utilisant l'ordre {{Times New Roman|I-V-vi-IV}} : : {{lien web | url = https://www.youtube.com/watch?v=oOlDewpCfZQ | titre = 4 Chords | Music Videos | The Axis Of Awesome | auteur = The Axis of Awesome | site = YouTube | date = 2011-07-20 | consulté le = 2020-12-17 }} {{boîte déroulante/début|titre=Chansons mêlées dans le sketch}} # Journey : ''Don't Stop Believing'' ; # James Blunt : ''You're Beautiful'' ; # Black Eyed Peas : ''Where Is the Love'' ; # Alphaville : ''Forever Young'' ; # Jason Mraz : ''I'm Yours'' ; # Train : ''Hey Soul Sister'' ; # The Calling : ''Wherever You Will Go'' ; # Elton John : ''Can You Feel The Love Tonight'' (''Le Roi lion'') ; # Akon : ''Don't Matter'' ; # John Denver : ''Take Me Home, Country Roads'' ; # Lady Gaga : ''Paparazzi'' ; # U2 : ''With Or Without You'' ; # The Last Goodnight : ''Pictures of You'' ; # Maroon Five : ''She Will Be Loved'' ; # The Beatles : ''Let It Be'' ; # Bob Marley : ''No Woman No Cry'' ; # Marcy Playground : ''Sex and Candy'' ; # Men At Work : ''Land Down Under'' ; # thème de ''America's Funniest Home Videos'' (équivalent des émissions ''Vidéo Gag'' et ''Drôle de vidéo'') ; # Jack Johnson : ''Taylor'' ; # Spice Girls : ''Two Become One'' ; # A Ha : ''Take On Me'' ; # Green Day : ''When I Come Around'' ; # Eagle Eye Cherry : ''Save Tonight'' ; # Toto : ''Africa'' ; # Beyonce : ''If I Were A Boy'' ; # Kelly Clarkson : ''Behind These Hazel Eyes'' ; # Jason DeRulo : ''In My Head'' ; # The Smashing Pumpkins : ''Bullet With Butterfly Wings'' ; # Joan Osborne : ''One Of Us'' ; # Avril Lavigne : ''Complicated'' ; # The Offspring : ''Self Esteem'' ; # The Offspring : ''You're Gonna Go Far Kid'' ; # Akon : ''Beautiful'' ; # Timberland featuring OneRepublic : ''Apologize'' ; # Eminem featuring Rihanna : ''Love the Way You Lie'' ; # Bon Jovi : ''It's My Life'' ; # Lady Gaga : ''Pokerface'' ; # Aqua : ''Barbie Girl'' ; # Red Hot Chili Peppers : ''Otherside'' ; # The Gregory Brothers : ''Double Rainbow'' ; # MGMT : ''Kids'' ; # Andrea Bocelli : ''Time To Say Goodbye'' ; # Robert Burns : ''Auld Lang Syne'' ; # Five for fighting : ''Superman'' ; # The Axis of Awesome : ''Birdplane'' ; # Missy Higgins : ''Scar''. {{boîte déroulante/fin}} Vous pouvez par exemple jouer les accords C-G-Am-F ({{Times New Roman|I-V-vi-IV}}) et chanter dessus ''{{lang|en|Let It Be}}'' (Paul McCartney, The Beattles, 1970) ou ''Libérée, délivrée'' (Robert Lopez, ''La Reine des neiges'', 2013). La progression {{Times New Roman|I-V-vi-IV}} est considérée comme « optimiste » tandis que sa variante {{Times New Roman|iv-IV-I-V}} est considérée comme « pessimiste ». On peut voir la progression {{Times New Roman|I-vi-IV-V}} comme une variante de l'anatole {{Times New Roman|I-vi-ii-V}}, obtenue en remplaçant l'accord de sustonique {{Times New Roman|ii}} par l'accord de sous-dominante {{Times New Roman|IV}} (son relatif majeur, et degré ayant la même fonction). ==== Exemples de progression selon le cercle des quintes en musique classique ==== [[Fichier:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.midi|vignette|Dietrich Buxtehude, Psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]] Cette progression selon la cercle des quintes, sous la forme {{Times New Roman|I-vi-IV-V}}, apparaît déjà au {{pc|xvii}}<sup>e</sup> siècle dans le psaume 42 ''Quem ad modum desiderat cervis'' (BuxVW92) de Dietrich Buxtehude (1637-1707). Le morceau est en ''fa'' majeur, la progression d'accords est donc F-Dm-B♭-C. : {{lien web | url = https://www.youtube.com/watch?v=8FmV9l1RqSg | titre = D. Buxtehude - Quemadmodum desiderat cervus, BuxWV 92 | auteur = Longobardo | site = YouTube | date = 2013-04-06 | consulté la = 2021-01-01 }} [[File:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.svg|vignette|450x450px|center|Dietrich Buxtehude, psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]] {{clear}} [[Fichier:JSBach BWV140 cantate 4 mesures.midi|vignette|J.-S. Bach, cantate BWV140, quatre premières mesures.]] On la trouve également dans l'ouverture de la cantate ''{{lang|de|Wachet auf, ruft uns die Stimme}}'' de Jean-Sébastien Bach (BWV140, 1731). Le morceau est en ''mi''♭ majeur, la progression d'accords est donc E♭-Cm-A♭<sup>6</sup>-B♭. [[Fichier:JSBach BWV140 cantate 4 mesures.svg|vignette|center|J.-S. Bach, cantate BWV140, quatre premières mesures.|alt=|517x517px]] {{clear}} [[Fichier:Mozart K310 Sonate8 mesures 45 a 49.midi|vignette|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]] La même progression est utilisée par Mozart, par exemple dans le premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778), la progression d'accords est C-Am-F-G qui correspond à la progression {{Times New Roman|III-i-VI-VII}} de ''la'' mineur, mais à la progression {{Times New Roman|I-vi-IV-V}} de la gamme relative, ''do'' majeur . [[Fichier:Mozart K310 Sonate8 mesures 45 a 49.svg|vignette|center|500px|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]] === Substitution tritonique === Un des accords les plus utilisés est donc l'accord de septième de dominante, {{Times New Roman|V<sup>7</sup><sub>+</sub>}} qui contient les degrés {{Times New Roman|V}}, {{Times New Roman|VII}}, {{Times New Roman|II}} ({{Times New Roman|IX}}) et {{Times New Roman|IV}}({{Times New Roman|XI}}) ; par exemple, en tonalité de ''do'' majeur, l'accord de ''sol'' septième (G<sup>7</sup>) contient les notes ''sol''-''si''-''ré''-''fa''. Si l'on prend l'accord dont la fondamentale est trois tons (triton) au-dessus ou en dessous — l'octave contenant six tons, on arrive sur la même note —, {{Times New Roman|♭II<sup>7</sup>}}, ici ''ré''♭ septième (D♭<sup>7</sup>), celui-ci contient les notes ''ré''♭-''fa''-''la''♭-''do''♭, cette dernière note étant l'enharmonique de ''si''. Les deux accords G<sup>7</sup> et D♭<sup>7</sup> ont donc deux notes en commun : le ''fa'' et le ''si''/''do''♭. Il est donc fréquent en jazz de substituer l'accord {{Times New Roman|V<sup>7</sup><sub>+</sub>}} par l'accord {{Times New Roman|♭II<sup>7</sup>}}. Par exemple, la progression {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|V<sup>7</sup>}}-{{Times New Roman|I<sup>&Delta;</sup>}} devient {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|♭II<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}}. C'est un procédé courant de réharmonisation (le fait de remplacer un accord par un autre dans un morceau existant). Les six substitutions possibles sont donc : C<sup>7</sup>↔F♯<sup>7</sup> - D♭<sup>7</sup>↔G<sup>7</sup> - D<sup>7</sup>↔A♭<sup>7</sup> - E♭<sup>7</sup>↔A<sup>7</sup> - E<sup>7</sup>↔B♭<sup>7</sup> - F<sup>7</sup>↔B<sup>7</sup>. [[Fichier:Übermäsiger Terzquartakkord.jpg|vignette|Exemple de cadence parfaite en ''do'' majeur avec substitution tritonique (sixte française).]] Dans l'accord D♭<sup>7</sup>, si l'on remplace le ''do''♭ par son ''si'' enharmonique, on obtient un accord de sixte augmentée : ''ré''♭-''fa''-''la''♭-''si''. Cet accord est utilisé en musique classique depuis la Renaissance ; on distingue en fait trois accords de sixte augmentée : * sixte française ''ré''♭-''fa''-''sol''-''si'' ; * sixte allemande : ''ré''♭-''fa''-''la''♭-''si'' ; * sixte italienne : ''ré''♭-''fa''-''si''. Par exemple, le ''Quintuor en ''ut'' majeur'' de Franz Schubert (1828) se termine par une cadence parfaite dont l'accord de dominante est remplacé par une sixte française ''ré''♭-''fa''-''si''-''sol''-''si'' (''ré''♭ aux violoncelles, ''fa'' à l'alto, ''si''-''sol'' aux seconds violons et ''si'' au premier violon). [[Fichier:Schubert C major Quintet ending.wav|vignette|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]] [[Fichier:Schubert C major Quintet ending.png|vignette|center|upright=2.5|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]] === Autres accords de substitution === Substituer un accord consiste à utiliser un accord provenant d'une tonalité étrangère à la tonalité en cours. À la différence d'une modulation, la substitution est très courte et ne donne pas l'impression de changer de tonalité ; on a juste un sentiment « étrange » passager. Un court passage dans une autre tonalité est également appelée « emprunt ». Nous avons déjà vu plusieurs méthodes de substitution : * utilisation d'une note étrangère : une note étrangère — note de passage, appoggiature, anticipation, retard… — crée momentanément un accord hors tonalité ; en musique classique, ceci n'est pas considéré comme un accord en propre, mais en jazz, on parle « d'accord de passage » et « d'accord suspendu » ; * utilisation d'une dominante secondaire : l'accord de dominante secondaire est hors tonalité ; le but ici est de faire une cadence parfaite, mais sur un autre degré que la tonique de la tonalité en cours ; * la substitution tritonique, vue ci-dessus, pour remplacer un accord de septième de dominante. Une dernière méthode consiste à remplacer un accord par un accord d'une gamme de même tonique, mais d'un autre mode ; on « emprunte » ''({{lang|en|borrow}})'' l'accord d'un autre mode. Par exemple, substituer un accord de la tonalité de ''do'' majeur par un accord de la tonalité de ''do'' mineur ou de ''do'' mode de ''mi'' (phrygien). Donc en ''do'' majeur, on peut remplacer un accord de ''ré'' mineur septième (D<sub>m</sub><sup>7</sup>) par un accord de ''ré'' demi-diminué (D<sup>⌀</sup>, D<sub>m</sub><sup>7&flat;5</sup>) qui est un accord appartenant à la donalité de ''la'' mineur harmonique. === Forme AABA === La forme AABA est composée de deux progressions de huit mesures, notées A et B ; cela représente trente-deux mesures au total, on parle donc souvent en anglais de la ''{{lang|en|32-bars form}}''. C'est une forme que l'on retrouve dans de nombreuses chanson de comédies musicales de Broadway comme ''Have You Met Miss Jones'' (''{{lang|en|I'd Rather Be Right}}'', 1937), ''{{lang|en|Over the Rainbow}}'' (''Le Magicien d'Oz'', Harold Harlen, 1939), ''{{lang|en|All the Things You Are}}'' (''{{lang|en|Very Warm for may}}'', 1939). Par exemple, la version de ''{{lang|en|Over the Rainbow}}'' chantée par Judy Garland est en ''la''♭ majeur et la progression d'accords est globalement : * A (couplet) : A♭-Fm | Cm-A♭ | D♭ | Cm-A♭ | D♭ | D♭-F | B♭-E♭ | A♭ * B (pont) : A♭ | B♭m | Cm | D♭ | A♭ | B♭-G | Cm-G | B♭m-E♭ soit en degrés : * A : {{Times New Roman|<nowiki>I-vi | iii-I | IV | iii-IV | IV | IV-vi | II-V | I</nowiki>}} * B : {{Times New Roman|<nowiki>I | ii | iii | IV | I | II-VII | iii-VII | ii-V</nowiki>}} Par rapport aux paroles de la chanson, on a * A : couplet 1 ''« {{lang|en|Somewhere […] lullaby}} »'' ; * A : couplet 2 ''« {{lang|en|Somewhere […] really do come true}} »'' ; * B : pont ''« {{lang|en|Someday […] you'll find me}} »'' ; * A : couplet 3 ''« {{lang|en|Somewhere […] oh why can't I?}} »'' ; : {{lien web | url = https://www.youtube.com/watch?v=1HRa4X07jdE | titre = Judy Garland - Over The Rainbow (Subtitles) | site = YouTube | auteur = Overtherainbow | consulté le = 2020-12-17 }} Une mise en œuvre de la forme AABA couramment utilisée en jazz est la forme anatole (à le pas confondre avec la succession d'accords du même nom), en anglais ''{{lang|en|rythm changes}}'' car elle s'inspire du morceau ''{{lang|en|I Got the Rythm}}'' de George Gerschwin (''Girl Crazy'', 1930) : * A : {{Times New Roman|I–vi–ii–V}} (succession d'accords « anatole ») ; * B : {{Times New Roman|III<sup>7</sup>–VI<sup>7</sup>–II<sup>7</sup>–V<sup>7</sup>}} (les fondamentales forment une succession de quartes, donc parcourent le « cercle des quintes » à l'envers). Par exemple, ''I Got the Rythm'' étant en ''ré''♭ majeur, la forme est : * A : D♭ - B♭m - E♭m - A♭ * B : F7 - B♭7 - E♭7 - A♭7 === Exemples === ==== Début du Largo de la symphonie du Nouveau Monde ==== [[File:Largo nouveau monde 5 1res mesures.svg|vignette|Partition avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]] [[File:Largo nouveau monde 5 1res mesures.midi|vignette|Fichier son avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]] Nous avons reproduit ci-contre les cinq premières mesure du deuxième mouvement Largo de la symphonie « Du Nouveau Monde » (symphonie n<sup>o</sup> 9 d'Antonín Dvořák, 1893). Cliquez sur l'image pour l'agrandir. Vous pouvez écouter cette partie jouée par un orchestre symphonique : * {{lien web |url =https://www.youtube.com/watch?v=y2Nw9r-F_yQ?t=565 |titre = Dvorak Symphony No.9 "From the New World" Karajan 1966 |site=YouTube (Seokjin Yoon) |consulté le=2020-12-11 }} (à 9 min 25), par le Berliner Philharmoniker, dirigé par Herbert von Karajan (1966) ; * {{lien web |url = https://www.youtube.com/watch?v=ASlch7R1Zvo |titre=Dvořák: Symphony №9, "From The New World" - II - Largo |site=YouTube (diesillamusicae) |consulté le=2020-12-11 }} : Wiener Philharmoniker, dirigé par Herbert von Karajan (1985). {{clear}} Cette partie fait intervenir onze instruments monodiques (ne jouant qu'une note à la fois) : des vents (trois bois, sept cuivres) et une percussion. Certains de ces instruments sont transpositeurs (les notes sur la partition ne sont pas les notes entendues). Jouées ensemble, ces onze lignes mélodiques forment des accords. Pour étudier cette partition, nous réécrivons les parties des instruments transpositeurs en ''do'' et les parties en clef d’''ut'' en clef de ''fa''. Nous regroupons les parties en clef de ''fa'' d'un côté et les parties en clef de ''sol'' d'un autre. {{boîte déroulante|Résultat|contenu=[[File:Largo nouveau monde 5 1res mesures transpositeurs en do.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde, en do.]]}} Nous pouvons alors tout regrouper sous la forme d'un système de deux portées clef de ''fa'' et clef de ''sol'', comme une partition de piano. {{boîte déroulante|Résultat|contenu= [[Fichier:Largo nouveau monde 5 1res mesures accords.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords.]] }} [[Fichier:Largo nouveau monde 5 1res mesures accords.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords.]] {{clear}} Ensuite, nous ne gardons que la basse et les notes médium. Nous changeons éventuellement certaines notes d'octave afin de n'avoir que des superpositions de tierce ou de quinte (état fondamental des accords, en faisant ressortir les notes manquantes). {{boîte déroulante|Résultat|contenu= [[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords simplifiés.]] }} [[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords simplifiés.]] Vous pouvez écouter cette partie jouée par un quintuor de cuivres (trompette, bugle, cor, trombone, tuba), donc avec des accords de cinq notes : : {{lien web |url=https://www.youtube.com/watch?v=pWfe60nbvjA |titre = Largo from The New World Symphony by Dvorak |site=YouTube (The Chamberlain Brass) |consulté le=2020-12-11 }} : The American Academy of Arts & Letters in New York City (2017). Nous allons maintenant chiffrer les accords. Pour établir la basse chiffrée, il nous faut déterminer le parcours harmonique. Pour le premier accord, les tonalités les plus simples avec un ''sol'' dièse sont ''la'' majeur et ''fa'' dièse mineur ; comme le ''mi'' est bécarre, nous retenons ''la'' majeur, il s'agit donc d'un accord de quinte sur la dominante (les accords de dominante étant très utilisés, cela nous conforte dans notre choix). Puis nous avons un ''si'' bémol, nous pouvons être en ''fa'' majeur ou en ''ré'' mineur ; nous retenons ''fa'' majeur, c'est donc le renversement d'un accord sur le degré {{Times New Roman|II}}. Dans la deuxième mesure, nous revenons en ''la'' majeur, puis, avec un ''la'' et un ''ré'' bémols, nous sommes en ''la'' bémol majeur ; nous avons donc un accord de neuvième incomplet sur la sensible, ou un accord de onzième incomplet sur la dominante. Dans la troisième mesure, nous passons en ''ré'' majeur, avec un accord de dominante. Puis, nous arrivons dans la tonalité principale, avec le renversement d'un accord de dominante sans tierce suivi d'un accord de tonique. Nous avons donc une cadence parfaite, conclusion logique d'une phrase. La progression des accords est donc : {| class="wikitable" ! scope="row" | Tonalité | ''la'' M - ''fa'' M || ''la'' M - ''la''♭ M || ''ré'' M - ''ré''♭ M || ''ré''♭ M |- ! scope="row" | Accords | {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|II}}<sup>6</sup><sub>4</sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|“V”}}<sup>9</sup><sub><s>5</s></sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|V}}<sup>+4</sup> || {{Times New Roman|I}}<sup>5</sup> |} Dans le chiffrage jazz, nous avons donc : * une triade de ''mi'' majeur, E ; * une triade de ''sol'' majeur avec un ''ré'' en basse : G/D ; * à nouveau un E ; * un accord de ''sol'' neuvième diminué incomplet, avec un ''ré'' bémol en basse : G dim<sup>9</sup>/D♭ ; * un accord de ''la'' majeur, A ; * un accord de ''la'' bémol septième avec une ''sol'' bémol à la basse : A♭<sup>7</sup>/G♭ ; * la partie se conclue par un accord parfait de ''ré''♭ majeur, D♭. Soit une progression E - G/D | E - G dim<sup>9</sup>/D♭ | A - A♭<sup>7</sup>/G♭ | D♭. [[Fichier:Largo nouveau monde 5 1res mesures accords chiffres.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde en accords simplifiés.]] {{clear}} ==== Thème de Smoke on the Water ==== Le morceau ''Smoke on the Water'' du groupe Deep Purple (album ''Machine Head'', 1972) possède un célèbre thème, un riff ''({{lang|en|rythmic figure}})'', joué à la guitare sous forme d'accords de puissance ''({{lang|en|power chords}})'', c'est-à-dire des accords sans tierce. Le morceau est en tonalité de ''sol'' mineur naturel (donc avec un ''fa''♮) avec ajout de la note bleue (''{{lang|en|blue note}}'', quinte diminuée, ''ré''♭), et les accords composant le thème sont G<sup>5</sup>, B♭<sup>5</sup>, C<sup>5</sup> et D♭<sup>5</sup>, ce dernier accord étant l'accord sur la note bleue et pouvant être considéré comme une appoggiature (indiqué entre parenthèse ci-après). On a donc ''a priori'', sur les deux premières mesures, une progression {{Times New Roman|I-III-IV}} puis {{Times New Roman|I-III-(♭V)-IV}}. Durant la majeure partie du thème, la guitare basse tient la note ''sol'' en pédale. {{note|En jazz, la qualité « <sup>5</sup> » indique que l'on n'a que la quinte (et donc pas la tierce), contrairement à la notation de basse chiffrée.}} : {{lien web | url = https://www.dailymotion.com/video/x5ili04 | titre = Deep Purple — Smoke on the Water (Live at Montreux 2006) | auteur = Deep Purple | site = Dailymotion | date = 2016 | consulté le = 2020-12-31 }} Cependant, cette progression forme une mélodie, on peut donc plus la voir comme un contrepoint, la superposition de deux voies ayant un mouvement conjoint, joué par un seul instrument, la guitare, la voie 2 étant jouée une quarte juste en dessous de la voie 1 (la quarte juste descendante étant le renversement de la quinte juste ascendante) : * voie 1 (aigu) : | ''sol'' - ''si''♭ - ''do'' | ''sol'' - ''si''♭ - (''ré''♭) - ''do'' | ; * voie 2 (grave) : | ''ré'' - ''fa'' - ''sol'' | ''ré'' - ''fa'' - (''la''♭) - ''sol'' |. En se basant sur la basse (''sol'' en pédale), nous pouvons considérer que ces deux mesures sont accompagnées d'un accord de Gm<sup>7</sup> (''sol''-''si''♭-''ré''-''fa''), chaque accord de la mélodie comprenant à chaque fois au moins une note de cet accord à l'exception de l'appogiature. {| class="wikitable" |+ Mise en évidence des notes de l'accord Gm<sup>7</sup> |- ! scope="row" | Accords | G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> |- ! scope="row" | Voie 1 | '''''sol''''' || '''''si''♭''' || ''do'' |- ! scope="row" | Voie 2 | '''''ré''''' || '''''fa''''' || '''''sol''''' |- ! scope="row" | Basse | '''''sol''''' || '''''sol''''' || '''''sol''''' |} Sur les deux mesures suivantes, la basse varie et suit les accords de la guitare avec un retard sur le dernier accord : {| class="wikitable" |+ Voies sur les mesure 3-4 du thème |- ! scope="row" | Accords | G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> || B♭<sup>5</sup> || G<sup>5</sup> |- ! scope="row" | Voie 1 | ''sol'' || ''si''♭ || ''do'' || ''si''♭ || ''sol'' |- ! scope="row" | Voie 2 | ''ré'' || ''fa'' || ''sol'' || ''fa'' || ''ré'' |- ! scope="row" | Basse | ''sol'' || ''sol'' || ''do'' || ''si''♭ || ''si''♭-''sol'' |} Le couplet de cette chanson est aussi organisé sur une progression de quatre mesures, la guitare faisant des arpèges sur les accords G<sup>5</sup> (''sol''-''ré''-''sol'') et F<sup>5</sup> (''fa''-''do''-''fa'') : : | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-F<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | soit une progression {{Times New Roman|<nowiki>| I-I | I-I | I-VII | I-I |</nowiki>}}. Nous pouvons aussi harmoniser le riff du thème sur cette progression, avec un accord F (''fa''-''la''-''do'') ; nous pouvons aussi nous rappeler que l'accord sur le degré {{Times New Roman|VII}} est plus volontiers considéré comme un accord de septième de dominante {{Times New Roman|V<sup>7</sup>}}, soit ici un accord Dm<sup>7</sup> (''ré''-''fa''-''la''-''do''). On peut donc considérer la progression harmonique sur le thème : : | Gm-Gm | Gm-Gm | Gm-F ou Dm<sup>7</sup> | Gm-Gm |. Cette analyse permet de proposer une harmonisation enrichie du morceau, tout en se rappelant qu'une des forces du morceau initial est justement la simplicité de sa structure, qui fait ressortir la virtuosité des musiciens. Nous pouvons ainsi comparer la version album à la version concert avec orchestre ou à la version latino de Pat Boone. À l'inverse, le groupe Psychostrip, dans une version grunge, a remplacé les accords par une ligne mélodique : * le thème ne contient plus qu'une seule voie (la guitare ne joue pas des accords de puissance) ; * dans les mesures 9 et 10, la deuxième guitare joue en contrepoint de type mouvement inverse, qui est en fait la voie 2 jouée en miroir ; * l'arpège sur le couplet est remplacé par une ligne mélodique en ostinato sur une gamme blues. {| class="wikitable" |+ Contrepoint sur les mesures 9 et 10 |- ! scope="row" | Guitare 1 | ''sol'' ↗ ''si''♭ ↗ ''do'' |- ! scope="row" | Guitare 2 | ''sol'' ↘ ''fa'' ↘ ''ré'' |} * {{lien web | url = https://www.dailymotion.com/video/x5ik234 | titre = Deep Purple — Smoke on the Water (In Concert with the London Symphony Orchestra, 1999) | auteur = Deep Purple | site = Dailymotion | date = 2016 | consulté le = 2020-12-31 }} * {{lien web | url = https://www.youtube.com/watch?v=MtUuNzVROIg | titre = Pat Boone — Smoke on the Water (In a Metal Mood, No More Mr. Nice Guy, 1997) | auteur = Orrore a 33 Giri | site = YouTube | date = 2019-06-24 | consulté le = 2020-12-31 }} * {{lien web | url = https://www.youtube.com/watch?v=n7zLlZ8B0Bk | titre = Smoke on the Water (Heroes, 1993) | auteur = Psychostrip | site = YouTube | date = 2018-06-20 | consulté le = 2020-12-31 }} == Accords et improvisation == Nous avons vu précédemment (chapitre ''[[../Gammes et intervalles#Modes et improvisation|Gammes et intervalles &gt; Modes et improvisation]]'') que le choix d'un mode adapté permet d'improviser sur un accord. L'harmonisation des gammes permet, en inversant le processus, d'étendre notre palette : il suffit de repérer l'accord sur une harmonisaiton de gamme, et d'utiliser cette gamme-là, dans le mode correspondant du degré de l'accord (voir ci-dessus ''[[#Harmonisation par des accords de septième|Harmonisation par des accords de septième]]''). Par exemple, nous avons vu que l'accord sur le septième degré d'une gamme majeure était un accord demi-diminué ; nous savons donc que sur un accord demi-diminué, nous pouvons improviser sur le mode correspondant au septième degré, soit le mode de ''si'' (locrien). Un accord de septième de dominante étant commun aux deux tonalités homonymes (par exemple ''fa'' majeur et ''fa'' mineur pour un ''do''<sup>7</sup><sub>+</sub> / C<sup>7</sup>), nous pouvons utiliser le mode de ''sol'' de la gamme majeure (mixolydien) ou de la gamme mineure mineure (mode phrygien dominant, ou phrygien espagnol) pour improviser. Mais l'accord de septième de dominante est aussi l'accord au début d'une grille blues ; on peut donc improviser avec une gamme blues, même si la tierce est majeure dans l'accord et mineure dans la gamme. [[Fichier:Mode improvisation accords do complet.svg]] == Autres accords courants == [[fichier:Cluster cdefg.png|vignette|Agrégat ''do - ré - mi - fa - sol''.]] Nous avons vu précédemment l'harmonisation des tonalités majeures et mineures harmoniques par des triades et des accords de septième ; certains accords étant rarement utilisés (l'accord sur le degré {{Times New Roman|III}} et, pour les tonalités mineures harmoniques, l'accord sur la tonique), certains accords étant utilisés comme des accords sur un autre degré (les accords sur la sensible étant considérés comme des accords de dominante sans fondamentale). Dans l'absolu, on peut utiliser n'importe quelle combinaison de notes, jusqu'aux agrégats, ou ''{{lang|en|clusters}}'' (mot anglais signifiant « amas », « grappe ») : un ensemble de notes contigües, séparées par des intervalles de seconde. Dans la pratique, on reste souvent sur des accords composés de superpositions de tierces, sauf dans le cas de transitions (voir la section ''[[#Notes étrangères|Notes étrangère]]''). === En musique classique === On utilise parfois des accords dont les notes ne sont pas dans la tonalité (hors modulation). Il peut s'agir d'accords de passage, de notes étrangères, par exemple utilisant un chromatisme (mouvement conjoint par demi-tons). Outre les accords de passage, les autres accords que l'on rencontre couramment en musique classique sont les accords de neuvième, et les accords de onzième et treizième sur tonique. Ces accords sont simplement obtenus en continuant à empiler les tierces. Il n'y a pas d'accord d'ordre supérieur car la quinzième est deux octaves au-dessus de la fondamentale. Comme pour les accords de septième, on distingue les accords de neuvième de dominante et les accords de neuvième d'espèce. Dans le cas de la neuvième de dominante, il y a une différence entre les tonalités majeures et mineures : l'intervalle de neuvième est respectivement majeur et mineur. Les chiffrages des renversements peuvent donc différer. Comme pour les accords de septième de dominante, on considère que les accords de septième sur le degré {{Times New Roman|VI}} sont en fait des accords de neuvième de dominante sans fondamentale. Les accords de neuvième d'espèce sont en général préparés et résolus. Préparés : la neuvième étant une note dissonante (c'est à une octave près la seconde de la fondamentale), l'accord qui précède doit contenir cette note, mais dans un accord consonant ; la neuvième est donc commune avec l'accord précédent. Résolus : la dissonance est résolue en abaissant la neuvième par un mouvement conjoint. Par exemple, en tonalité de ''do'' majeur, si l'on veut utiliser un accord de neuvième d'espèce sur la tonique ''(do - mi - sol - si - ré)'', on peut utiliser avant un accord de dominante ''(sol - si - ré)'' en préparation puis un accord parfait sur le degré {{Times New Roman|IV}} ''(fa - la - do)'' en résolution ; nous avons donc sur la voie la plus aigüe la succession ''ré'' (consonant) - ''ré'' (dissonant) - ''do'' (consonant). On rencontre également parfois des accords de onzième et de treizième. On omet en général la tierce, car elle est dissonante avec la onzième. L'accord le plus fréquemment rencontré est l'accord sur la tonique : on considère alors que c'est un accord sur la dominante que l'on a enrichi « par le bas », en ajoutant une quinte inférieure. par exemple, dans la tonalité de ''do'' majeur, l'accord ''do - sol - si - ré - fa'' est considéré comme un accord de septième de dominante sur tonique, le degré étant noté « {{Times New Roman|V}}/{{Times New Roman|I}} ». De même pour l'accord ''do - sol - si - ré - fa - la'' qui est considéré comme un accord de neuvième de dominante sur tonique. === En jazz === En jazz, on utilise fréquemment l'accord de sixte à la place de l'accord de septième majeure sur la tonique. Par exemple, en ''do'' majeur, on utilise l'accord C<sup>6</sup> ''(do - mi - sol - la)'' à la place de C<sup>Δ</sup> ''(do - mi - sol - si)''. On peut noter que C<sup>6</sup> est un renversement de Am<sup>7</sup> et pourrait donc se noter Am<sup>7</sup>/C ; cependant, le fait de le noter C<sup>6</sup> indique que l'on a bien un accord sur la tonique qui s'inscrit dans la tonalité de ''do'' majeur (et non, par exemple, de ''la'' mineur naturelle) — par rapport à l'harmonie fonctionnelle, on remarquera que Am<sup>7</sup> a une fonction tonique, l'utilisation d'un renversement de Am<sup>7</sup> à la place d'un accord de C<sup>Δ</sup> est donc logique. Les accords de neuvième, onzième et treizième sont utilisés comme accords de septième enrichis. Le chiffrage suit les règles habituelles : on ajoute un « 9 », un « 11 » ou un « 13 » au chiffrage de l'accord de septième. On utilise également des accords dits « suspendus » : ce sont des accords de transition qui sont obtenus en prenant une triade majeure ou mineure et en remplaçant la tierce par la quarte juste (cas le plus fréquent) ou la seconde majeure. Plus particulièrement, lorsque l'on parle simplement « d'accord suspendu » sans plus de précision, cela désigne l'accord de neuvième avec une quarte suspendue, noté « 9sus4 » ou simplement « sus ». == L'harmonie tonale == L'harmonie tonale est un ensemble de règle assez strictes qui s'appliquent dans la musique savante européenne, de la période baroque à la période classique classique ({{pc|xiv}}<sup>e</sup>-{{pc|xviii}}<sup>e</sup> siècle). Certaines règles sont encore largement appliquées dans divers styles musicaux actuels, y compris populaire (rock, rap…), d'autres sont au contraire ignorées (par exemple, un enchaînement de plusieurs accords de même qualité forme un mouvement parallèle, ce qui est proscrit en harmonie tonale). De nos jours, on peut voir ces règles comme des règles « de bon goût », et leur application stricte comme une manière de composer « à la manière de ». Précédemment, nous avons vu la progression des accords. Ci-après, nous abordons aussi la manière dont les notes de l'accord sont réparties entre plusieurs voix, et comment on construit chaque voix. === Concepts fondamentaux === ; Consonance : Les intervalles sont considérés comme « plus ou moins consonants » : :* consonance parfaite : unisson, quinte et octave ; :* consonance mixte (parfaite dans certains contextes, imparfaite dans d'autres) : quarte ; :* consonance imparfaite : tierce et sixte ; :* dissonance : seconde et septième. ; Degrés : Certains degrés sont considérés comme « forts », « meilleurs », ce sont les « notes tonales » : {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (sous-dominante) et {{Times New Roman|V}} (dominante). [[Fichier:Mouvements harmoniques.svg|vignette|upright=0.75|Mouvements harmoniques.]] ; Mouvements : Le mouvement décrit la manière dont les voix évoluent les unes par rapport aux autres : :# Mouvement parallèle : les voix sont séparées par un intervalle constant. :# Mouvement oblique : une voix reste constante, c'est le bourdon ; l'autre monte ou descend. :# Mouvement contraire : une voix descend, l'autre monte. :# Échange de voix : les voix échangent de note ; les mélodies se croisent mais on a toujours le même intervalle harmonique. {{clear}} === Premières règles === ; Règle du plus court chemin : Quand on passe d'un accord à l'autre, la répartition des notes se fait de sorte que chaque voix fait le plus petit mouvement possible. Notamment : si les deux accords ont des notes en commun, alors les voix concernées gardent la même note. : Les deux voix les plus importantes sont la voix aigüe — soprano — et la voix la plus grave — basse. Ces deux voix sont relativement libres : la voix de soprano a la mélodie, la voix de basse fonde l'harmonie. La règle du plus court chemin s'applique surtout aux voix intermédiaires ; si l'on a des mouvements conjoints, ou du moins de petits intervalles — c'est le sens de la règle du plus court chemin —, alors les voix sont plus faciles à interpréter. Cette règle évite également que les voix n'empiètent l'une sur l'autre (voir la règle « éviter le croisement des voix »). ; Éviter les consonances parfaites consécutives :* Lorsque deux voix sont à l'unisson ou à l'octave, elles ne doivent pas garder le même intervalle, l'effet serait trop plat. :* Lorsque deux voix sont à la quarte ou à la quinte, elles ne doivent pas garder le même intervalle, car l'effet est trop dur. : Pour éviter cela, lorsque l'on part d'un intervalle juste, on a intérêt à pratiquer un mouvement contraire aux voix qui ne gardent pas la même note, ou au moins un mouvement direct : les voix vont dans le même sens, mais l'intervalle change. : Notez que même avec le mouvement contraire, on peut avoir des consonances parfaites consécutives, par exemple si une voix fait ''do'' aigu ↗ ''sol'' aigu et l'autre ''sol'' médium ↘ ''do'' grave. : L'interdiction des consonances parfaites consécutives n'a pas été toujours appliquée, le mouvement parallèle strict a d'ailleurs été le premier procédé utilisé dans la musique religieuse au {{pc|x}}<sup>e</sup> siècle. On peut par exemple utiliser des quintes parallèles pour donner un style médiéval au morceau. On peut également utiliser des octaves parallèles sur plusieurs notes afin de créer un effet de renforcement de la mélodie. : Par ailleurs, les consonances parfaites consécutives sont acceptées lorsqu'il s'agit d'une cadence (transition entre deux parties ou bien conclusion du morceau). ; Éviter le croisement des voix : Les voix sont organisées de la plus grave à la plus aigüe. Deux voix n'étant pas à l'unisson, celle qui est plus aigüe ne doit pas devenir la plus grave et ''vice versa''. ; Soigner la partie soprano : Comme c'est celle qu'on entend le mieux, c'est en général celle qui porte la mélodie principale. On lui applique des règles spécifiques : :# Si elle chante la sensible dans un accord de dominante ({{Times New Roman|V}}), alors elle doit monter à la tonique, c'est-à-dire que la note suivante sera la tonique située un demi-ton au dessus. :# Si l'on arrive à une quinte ou une octave entre les parties basse et soprano par un mouvement direct, alors sur la partie soprano, le mouvement doit être conjoint. On doit donc arriver à cette situation par des notes voisines au soprano. ; Préférer certains accords : Les deux degrés les plus importants sont la tonique ({{Times New Roman|I}}) et la dominante ({{Times New Roman|V}}), les accords correspondants ont donc une importance particulière. : À l'inverse, l'accord de sensible ({{Times New Roman|VII}}) n'est pas considéré comme ayant une fonction harmonique forte. On le considère comme un accord de dominante affaibli. En tonalité mineure, on évite également l'accord de médiante ({{Times New Roman|III}}). : Donc on utilise en priorité les accords de : :# {{Times New Roman|I}} et {{Times New Roman|V}}. :# Puis {{Times New Roman|II}}, {{Times New Roman|IV}}, {{Times New Roman|VI}} ; et {{Times New Roman|III}} en mode majeur. :# On évite {{Times New Roman|VII}} ; et {{Times New Roman|III}} en mode mineur. ; Préférer certains enchaînements : Les enchaînements d'accord peuvent être classés par ordre de préférence. Par ordre de préférence décroissante (du « meilleur » au « moins bon ») : :# Meilleurs enchaînements : quarte ascendante ou descendante. Notons que la quarte est le renversement de la quinte, on a donc des enchaînements stables et naturels, mais avec un intervalle plus court qu'un enchaînement de quintes. :# Bons enchaînements : tierce ascendante ou descendante. Les accords consécutifs ont deux notes en commun. :# Enchaînements médiocres : seconde ascendante ou descendante. Les accords sont voisins, mais ils n'ont aucune note en commun. On les utilise de préférence en mouvement ascendant, et on utilise surtout les enchaînements {{Times New Roman|IV}}-{{Times New Roman|V}}, {{Times New Roman|V}}-{{Times New Roman|VI}} et éventuellement {{Times New Roman|I}}-{{Times New Roman|II}}. :# Les autres enchaînements sont à éviter. : On peut atténuer l'effet d'un enchaînement médiocre en plaçant le second accord sur un temps faible ou bien en passant par un accord intermédiaire. [[Fichier:Progression Vplus4 I6.svg|thumb|Résolution d'un accord de triton (quarte sensible) vers l'accord de sixte de la tonique.]] ; La septième descend par mouvement conjoint : Dans un accord de septième de dominante, la septième — qui est donc le degré {{Times New Roman|IV}} — descend par mouvement conjoint — elle est donc suivie du degré {{Times New Roman|III}}. : Corolaire : un accord {{Times New Roman|V}}<sup>+4</sup> se résout par un accord {{Times New Roman|I}}<sup>6</sup> : on a bien un enchaînement {{Times New Roman|V}} → {{Times New Roman|I}}, et la 7{{e}} (degré {{Times New Roman|IV}}), qui est la basse de l'accord {{Times New Roman|V}}<sup>+4</sup>, descend d'un degré pour donner la basse de l'accord {{Times New Roman|I}}<sup>6</sup> (degré {{Times New Roman|III}}). {{clear}} [[Fichier:Progression I64 V7plus I5.svg|thumb|Accord de sixte et de quarte cadentiel.]] ; Un accord de sixte et quarte est un accord de passage : Le second renversement d'un accord parfait est soit une appoggiature, soit un accord de passage, soit un accord de broderie. : S'il s'agit de l'accord de tonique {{Times New Roman|I}}<sup>6</sup><sub>4</sub>, c'est « accord de sixte et quarte de cadence », l'appoggiature de l'accord de dominante de la cadence parfaite. {{clear}} Mais il faut appliquer ces règles avec discernement. Par exemple, la voix la plus aigüe est celle qui s'entend le mieux, c'est donc elle qui porte la mélodie principale. Il est important qu'elle reste la plus aigüe. La voix la plus grave porte l'harmonie, elle pose les accords, il est donc également important qu'elle reste la plus grave. Ceci a deux conséquences : # Ces deux voix extrêmes peuvent avoir des intervalles mélodiques importants et donc déroger à la règle du plus court chemin : la voix aigüe parce que la mélodie prime, la voix de basse parce que la progression d'accords prime. # Les croisements des voix intermédiaires sont moins critiques. Par ailleurs, si l'on applique strictement toutes les règles « meilleurs accords, meilleurs enchaînements », on produit un effet conventionnel, stéréotypé. Il est donc important d'utiliser les solutions « moins bonnes », « médiocres » pour apporter de la variété. Ajoutons que les renversements d'accords permettent d'avoir plus de souplesse : on reste sur le même accord, mais on enrichit la mélodie sur chaque voix. Le ''Bolero'' de Maurice Ravel (1928) brise un certain nombre de ces règles. Par exemple, de la mesure 39 à la mesure 59, la harpe joue des secondes. De la mesure 149 à la mesure 165, les piccolo jouent à la sixte, dans des mouvement strictement parallèle, ce qui donne d'ailleurs une sonorité étrange. À partir de la mesure 239, de nombreux instruments jouent en mouvement parallèles (piccolos, flûtes, hautbois, cor, clarinettes et violons). === Application === [[Fichier:Harmonisation possible de frere jacques exercice.svg|vignette|Exercice : harmoniser ''Frère Jacques''.]] Harmoniser ''Frère Jacques''. Nous considérons un morceau à quatre voix : basse, ténor, alto et soprano. La soprano chante la mélodie de ''Frère Jacques''. L'exercice consiste à proposer l'écriture des trois autres voix en respectant les règles énoncées ci-dessus. Pour simplifier, nous ajoutons les contraintes suivantes : * toutes les voix chantent des blanches ; * nous nous limitons aux accords de quinte (accords de trois sons composés d'une tierce et d'une quinte) sans avoir recours à leurs renversements (accords de sixte, accords de sixte et de quarte). Les notes à gauche de la portée indiquent la tessiture (ou ambitus), l'amplitude que peut chanter la voix. {{clear}} {{boîte déroulante/début|titre=Solution possible}} [[Fichier:Harmonisation possible de frere jacques solution.svg|vignette|Harmonisation possible de ''Frère Jacques'' (solution de l'exercice).]] Il n'y a pas qu'une solution possible. Le premier accord doit contenir un ''do''. Nous sommes manifestement en tonalité de ''do'' majeur, nous proposons de commencer par l'accord parfait de ''do'' majeur, I<sup>5</sup>. Le deuxième accord doit comporter un ''ré''. Si nous utilisons l'accord de quinte de ''ré'', nous allons créer une quinte parallèle. Nous pourrions utiliser un renversement, mais nous nous imposons de chercher un autre accord. Il peut s'agir de l'accord ''si''<sup>5</sup> ''(si-ré-fa)'' ou de l'accord de ''sol''<sup>5</sup> ''(sol-si-ré)''. La dernière solution permet d'utiliser l'accord de dominante qui est un accord important de la tonalité. La règle du plus court chemin imposerait le ''sol'' grave pour la partie de basse, mais cela est proche de la limite du chanteur, nous préférons passer au ''sol'' aigu, plus facile à chanter. Nous vérifions qu'il n'y a pas de quinte parallèle : l'intervalle ascendant ''do-sol'' (basse-alto) devient ''sol-si'' (3<sup>ce</sup>), l'intervalle descendant ''do-sol'' (soprano-alto) devient ''ré-si'' (3<sup>ce</sup>). De la même manière, pour le troisième accord, nous ne pouvons pas passer à un accord de ''la''<sup>5</sup> pour éviter une quinte parallèle. Nous avons le choix entre ''do''<sup>5</sup> ''(do-mi-sol)'' et ''mi''<sup>5</sup> ''(mi-sol-si)''. Nous préférons revenir à l'accord de fondamental, solution très stable (l'enchaînement {{Times New Roman|V}}-{{Times New Roman|I}} formant une cadence parfaite). Pour le quatrième accord, nous pourrions rester sur l'accord parfait de ''do'' mais cela planterait en quelque sorte la fin du morceau puisque l'on resterait sur la cadence parfaite ; or, nous connaissons le morceau et savons qu'il n'est pas fini. Nous choisissons l'accord de ''la''<sup>5</sup> qui est une sixte ascendante ({{Times New Roman|I}}-{{Times New Roman|VI}}). Nos aurions pu répartir les voix différemment. Par exemple : * alto : ''sol''-''si''-''sol''-''do'' ; * ténor : ''mi''-''ré''-''mi''-''mi''. {{boîte déroulante/fin}} [[Fichier:Harmonisation possible de frere jacques.midi|vignette|Fichier son correspondant.]] {{clear}} == Annexe == === Accords en musique classique === Un accord est un ensemble de notes jouées simultanément. Il peut s'agir : * de notes jouées par plusieurs instruments ; * de notes jouées par un même instrument : piano, clavecin, orgue, guitare, harpe (la plupart des instruments à clavier et des instruments à corde). Pour deux notes jouées simultanément, on parle d'intervalle « harmonique » (par opposition à l'intervalle « mélodique » qui concerne les notes jouées successivement). Les notes répétées à différentes octaves ne changent pas la nature de l'accord. La musique classique considère en général des empilements de tierces ; un accord de trois notes sera constitué de deux tierces successives, un accord de quatre notes de trois tierces… Lorsque tous les intervalles sont des intervalles impairs — tierces, quintes, septièmes, neuvièmes, onzièmes, treizièmes… — alors l'accord est dit « à l'état fondamental » (ou encore « primitif » ou « direct »). La note de la plus grave est appelée « fondamentale » de l'accord. Lorsque l'accord comporte un ou des intervalles pairs, l'accord est dit « renversé » ; la note la plus grave est appelée « basse ». De manière plus générale, l'accord est dit à l'état fondamental lorsque la basse est aussi la fondamentale. On a donc un état idéal de l'accord (état canonique) — un empilement strict de tierces — et l'état réel de l'accord — l'empilement des notes réellement jouées, avec d'éventuels redoublements, omissions et inversions ; et seule la basse indique si l'accord est à l'état fondamental ou renversé. Le chiffrage dit de « basse continue » ''({{lang|it|basso continuo}})'' désigne la représentation d'un accord sous la forme d'un ou plusieurs chiffres arabes et éventuellement d'un chiffre romain. ==== Accords de trois notes ==== En musique classique, les seuls accords considérés comme parfaitement consonants, c'est-à-dire sonnant agréablement à l'oreille, sont appelés « accords parfaits ». Si l'on prend une tonalité et un mode donné, alors l'accord construit par superposition es degrés I, III et V de cette gamme porte le nom de la gamme qui l'a généré. [[fichier:Accord do majeur chiffre.svg|vignette|upright=0.5|Accord parfait de ''do'' majeur chiffré.]] Par exemple : * « l'accord parfait de ''do'' majeur » est composé des notes ''do'', ''mi'' et ''sol'' ; * « l'accord parfait de ''la'' mineur » est composé des notes ''la'', ''do'' et ''mi''. Un accord parfait majeur est donc composé, en partant de la fondamentale, d'une tierce majeure et d'une quinte juste. Un accord parfait mineur est composé d'une tierce mineure et d'une quinte juste. L'accord parfait à l'état fondamental est appelé « accord de quinte » et est simplement chiffré « 5 » pour indiquer la quinte. On peut également commencer un accord sur sa deuxième ou sa troisième note, en faisant monter celle(s) qui précède(nt) à l'octave suivante. On parle alors de « renversement d'accord » ou d'accord « renversé ». [[Fichier:Accord do majeur renversements chiffre.svg|vignette|upright=0.75|Accord parfait de ''do'' majeur et ses renversements, chiffrés.]] Par exemple, * le premier renversement de l'accord parfait de ''do'' majeur est :<br /> ''mi'', ''sol'', ''do'' ; * le second renversement de l'accord parfait de do majeur est :<br /> ''sol'', ''do'', ''mi''. Les notes conservent leur nom de « fondamentale », « tierce » et « quinte » malgré le changement d'ordre. La note la plus grave est appelée « basse ». Dans le cas du premier renversement, le deuxième note est la tierce de la basse (la note la plus grave) et la troisième note est la sixte ; le chiffrage en chiffres arabes est donc « 6 » (puisque l'on omet la tierce) et l'accord est appelé « accord de sixte ». Pour le deuxième renversement, les intervalles sont la quarte et la sixte, le chiffrage est donc « 6-4 » et l'accord est appelé « accord de sixte et de quarte ». Dans tous les cas, on chiffre le degré on considérant la fondamentale, par exemple {{Times New Roman|I}} si l'accord est construit sur la tonique de la gamme. Les autres accords de trois notes que l'on rencontre sont : * l'accord de quinte diminuée, constitué d'une tierce mineure et d'une quinte diminuée ; lorsqu'il est construit sur le septième degré d'une gamme, on considère que c'est un accord de septième de dominante sans fondamentale (voir plus bas), le degré est donc indiqué « “{{Times New Roman|V}}” » (cinq entre guillemets) et non « {{Times New Roman|VII}} » ; * l'accord de quinte augmenté : il est composé d'une tierce majeure et qu'une quinte augmentée. Dans le tableau ci-dessous, * « m » désigne un intervalle mineur ; * « M » un intervalle majeur ou le mode majeur ; * « J » un intervalle juste ; * « d » un intervalle diminué ; * « A » un intervalle augmenté ; * « mh » le mode mineur harmonique ; * « ma » le mode mineur ascendant ; * « md » le mode mineur descendant. {| class="wikitable" |+ Accords de trois notes ! scope="col" rowspan="2" | Nom ! scope="col" rowspan="2" | 3<sup>ce</sup> ! scope="col" rowspan="2" | 5<sup>te</sup> ! scope="col" rowspan="2" | État fondamental ! scope="col" rowspan="2" | 1<sup>er</sup> renversement ! scope="col" rowspan="2" | 2<sup>nd</sup> renversement ! scope="col" colspan="4"| Construit sur les degrés |- ! scope="col" | M ! scope="col" | mh ! scope="col" | ma ! scope="col" | md |- | Accord parfait<br /> majeur || M || J | accord de quinte || accord de sixte || accord de<br />sixte et de quarte | {{Times New Roman|I, IV, V}} || {{Times New Roman|V, VI}} || {{Times New Roman|IV, V}} || {{Times New Roman|III, VI, VII}} |- | Accord parfait<br /> mineur || m || J | accord de quinte || accord de sixte || accord de<br />sixte et de quarte | {{Times New Roman|II, III, VI}} || {{Times New Roman|I, IV}} || {{Times New Roman|I, II}} || {{Times New Roman|I, IV, V}} |- | Accord de<br />quinte diminuée || m || d | accord de<br />quinte diminuée || accord de<br />sixte sensible<br />sans fondamentale || accord de triton<br />sans fondamentale | {{Times New Roman|VII (“V”)}} || {{Times New Roman|II, VII (“V”)}} || {{Times New Roman|VI, VII (“V”)}} || {{Times New Roman|II}} |- | Accord de<br />quinte augmentée || M || A | accord de<br />quinte augmentée || accord de sixte<br />et de tierce sensible || accord de sixte et de quarte<br />sur sensible | &nbsp; || {{Times New Roman|III}} || {{Times New Roman|III}} || &nbsp; |} ==== Accords de quatre notes ==== Les accords de quatre notes sont des accord composés de trois tierces superposées. La dernière note étant le septième degré de la gamme, on parle aussi d'accords de septième. Ces accords sont dissonants : ils contiennent un intervalle de septième (soit une octave montante suivie d'une seconde descendante). Ils laissent donc une impression de « tension ». Il existe sept différents types d'accords, ou « espèces ». Citons l'accord de septième de dominante, l'accord de septième mineure et l'accord de septième majeure. ===== L'accord de septième de dominante ===== [[Fichier:Accord 7e dominante do majeur renversements chiffre.svg|vignette|Accord de septième de dominante de ''do'' majeur et ses renversements, chiffrés.]] L'accord de septième de dominante est l'empilement de trois tierces à partir de la dominante de la gamme, c'est-à-dire du {{Times New Roman|V}}<sup>e</sup> degré. Par exemple, l'accord de septième de dominante de ''do'' majeur est l'accord ''sol''-''si''-''ré''-''fa'', et l'accord de septième de dominante de ''la'' mineur est ''mi''-''sol''♯-''si''-''ré''. L'accord de septième de dominante dont la fondamentale est ''do'' (''do''-''mi''-''sol''-''si''♭) appartient à la gamme de ''fa'' majeur. Que le mode soit majeur ou mineur, il est composé d'une tierce majeure, d'une quinte juste et d'une septième mineure (c'est un accord parfait majeur auquel on ajoute une septième mineure). C'est de loin l'accord de septième le plus utilisé ; il apparaît au {{pc|xvii}}<sup>e</sup> en musique classique. Dans son état fondamental, son chiffrage est {{Times New Roman|V 7/+}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}). Le signe plus indique la sensible. Son premier renversement est appelé « accord de quinte diminuée et sixte » et est noté {{Times New Roman|V 6/<s>5</s>}} (ou {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}). Son deuxième renversement est appelé « accord de sixte sensible », puisque la sixte de l'accord est la sensible de la gamme, et est noté {{Times New Roman|V +6}} (ou {{Times New Roman|V<sup>+6</sup>}}). Son troisième renversement est appelé « accord de quarte sensible » et est noté {{Times New Roman|V +4}} (ou {{Times New Roman|V<sup>+4</sup>}}). [[Fichier:Accord 7e dominante sans fondamentale do majeur renversements chiffre.svg|vignette|Accord de septième de dominante sans fondamentale de ''do'' majeur et ses renversements, chiffrés.]] On utilise aussi l'accord de septième de dominante sans fondamentale ; c'est alors un accord de trois notes. Dans son état fondamental, c'est un « accord de quinte diminuée » placé sur le {{Times New Roman|VII}}<sup>e</sup> degré (mais c'est bien un accord construit sur le {{Times New Roman|V}}<sup>e</sup> degré), noté {{Times New Roman|“V” <s>5</s>}} (ou {{Times New Roman|“V”<sup><s>5</s></sup>}}). Notez les guillemets qui indiquent que la fondamentale V est absente. Dans son premier renversement, c'est un « accord de sixte sensible sans fondamentale » noté {{Times New Roman|“V” +6/3}} (ou {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}}). Dans son second renversement, c'est un « accord de triton sans fondamentale » (puisque le premier intervalle est une quarte augmentée qui comporte trois tons) noté {{Times New Roman|“V” 6/+4}} (ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}). Notons qu'un accord de septième de dominante n'a pas toujours la dominante pour fondamentale : tout accord composé d'une tierce majeure, d'une quinte juste et d'une septième mineure est un accord de septième de dominante et est chiffré {{Times New Roman|<sup>7</sup><sub>+</sub>}}, quel que soit le degré sur lequel il est bâti (certaines notes peuvent avoir une altération accidentelle). ===== Les accords de septième d'espèce ===== Les autres accords de septièmes sont dits « d'espèce ». L'accord de septième mineure est l'accord de septième formé sur la fondamentale d'une gamme mineure ''naturelle''. Par exemple, l'accord de septième mineure de ''la'' est ''la''-''do''-''mi''-''sol''. Il est composé d'une tierce mineure, d'une quinte juste et d'une septième mineure (c'est un accord parfait mineur auquel on ajoute une septième mineure). L'accord de septième majeure est l'accord de septième formé sur la fondamentale d'une gamme majeure. Par exemple, L'accord de septième majeure de ''do'' est ''do''-''mi''-''sol''-''si''. Il est composé d'une tierce majeure, d'une quinte juste et d'une septième majeure (c'est un accord parfait majeur auquel on ajoute une septième majeure). ==== Utilisation du chiffrage ==== Le chiffrage est utilisé de deux manières. La première manière, c'est la notation de la basse continue. La basse continue est une technique d'improvisation utilisée dans le baroque pour l'accompagnement d'instruments solistes. Sur la partition, on indique en général la note de basse de l'accord et le chiffrage en chiffres arabes. La seconde manière, c'est pour l'analyse d'une partition. Le fait de chiffrer les accords permet de mieux en comprendre la structure. De manière générale, on peut retenir que : * le chiffrage « 5 » indique un accord parfait, superposition d'une tierce (majeure ou mineure) et d'une quinte juste ; * le chiffrage « 6 » indique le premier renversement d'un accord parfait ; * le chiffrage « 6/4 » indique le second renversement d'un accord parfait ; * chiffrage « 7/+ » indique un accord de septième de dominante ; * le signe « + » indique en général que la note de l'intervalle est la sensible ; * un intervalle barré désigne un intervalle diminué. [[fichier:Accords gamme do majeur la mineur.svg|class=transparent| center | Principaux accords construits sur les gammes de ''do'' majeur et de ''la'' mineur harmonique.]] === Notation « jazz » === En jazz et de manière générale en musique rock et populaire, la base d'un accord est la triade composée d'une tierce (majeure ou mineure) et d'une quinte juste. Pour désigner un accord, on utilise la note fondamentale, éventuellement désigné par une lettre dans le système anglo-saxon (A pour ''la'' etc.), suivi d'une qualité (comme « m », « + »…). Les renversements ne sont pas notés de manière particulière, ils sont notés comme les formes fondamentales. Dans les deux tableaux suivants, la fondamentale est notée X (remplace le C pour un accord de ''do'', le D pour un accord de ''ré''…). La construction des accords est décrite par la suite. [[Fichier:Arbre accords triades 5d5J5A.svg|vignette|upright=1.5|Formation des triades présentée sous forme d'arbre.]] {| class="wikitable" |+ Notation des principales triades |- | ! scope="col" | Tierce<br />mineure (3m) ! scope="col" | Tierce<br />majeure (3M) |- ! scope="row" | Quinte diminuée (5d) | X<sup>o</sup>, Xm<sup>♭5</sup>, X–<sup>♭5</sup> || |- ! scope="row" | Quinte juste (5J) | Xm, X– || X |- ! scope="row" | Quinte augmentée (5A) | || X+, X<sup>♯5</sup> |} [[Fichier:Triades do.svg|class=transparent|center|Triades de do.]] {| class="wikitable" |+ Notation des principaux accords de septième |- | colspan="2" | ! scope="col" | Tierce<br />mineure (3m) ! scope="col" | Tierce<br />majeure (3M) |- ! scope="row" rowspan="2" | Quinte<br />diminuée (5d) ! scope="row" | Septième diminuée (7d) | X<sup>o7</sup> || |- ! scope="row" | Septième mineure (7m) | Xm<sup>7(♭5)</sup>, X–<sup>7(♭5)</sup>, X<sup>Ø</sup> || |- ! scope="row" rowspan="3" | Quinte<br />juste (5J) ! scope="row" | Sixte majeure (6M) | Xm<sup>6</sup> || X<sup>6</sup> |- ! scope="row" | Septième mineure (7m) | Xm<sup>7</sup>, X–<sup>7</sup> || X<sup>7</sup> |- ! scope="row" | Septième majeure (7M) | Xm<sup>maj7</sup>, X–<sup>maj7</sup>, Xm<sup>Δ</sup>, X–<sup>Δ</sup> || X<sup>maj7</sup>, X<sup>Δ</sup> |- ! scope="row" rowspan="2" | Quinte<br />augmentée (5A) ! scope="row" | Septième mineure (7m) | || X+<sup>7</sup> |- ! scope="row" | Septième majeure (7M) | || X+<sup>maj7</sup> |} [[Fichier:Arbre accords septieme.svg|class=transparent|center|Formation des accords de septième présentée sous forme d'arbre.]] &nbsp; &nbsp; [[Fichier:Accords do septieme.svg|class=transparent|center|Accord de do septième.]] On notera que l'intervalle de sixte majeure est l'enharmonique de celui de septième diminuée (6M = 7d). [[File:Principaux accords do.svg|class=transparent|center|Principaux accords de do.]] ==== Triades ==== ; Accords fondés sur une tierce majeure * accord parfait majeur : pas de notation *: p. ex. « ''do'' » ou « C » pour l'accord parfait de ''do'' majeur (''do'' - ''mi'' - ''sol'') ; Accords fondés sur une tierce mineure * accord parfait mineur : « m », « min » ou « – » *: « ''do'' m », « ''do'' – », « Cm », « C– »… pour l'accord parfait de ''do'' mineur (''do'' - ''mi''♭ - ''sol'') ==== Triades modifiées ==== ; Accords fondés sur une tierce majeure * accord augmenté (la quinte est augmentée) : aug, +, ♯5 *: « ''do'' aug », « ''do'' + », « ''do''<sup>♯5</sup> » « Caug », « C+ » ou « C<sup>♯5</sup> » pour l'accord de ''do'' augmenté (''do'' - ''mi'' - ''sol''♯) : L'accord augmenté est un empilement de tierces majeures. Ainsi, un accord augmenté a deux notes communes avec deux autres accords augmentés : C+ (''do'' - ''mi'' - ''sol''♯) a deux notes communes avec A♭+ (''la''♭ - ''do'' - ''mi'') et avec E+ (''mi'' - ''sol''♯ - ''si''♯) ; et on remarque que ces trois accords sont en fait enharmoniques (avec les enharmonies ''la''♭ = ''sol''♯ et ''si''♯ = ''do''). En effet, l'octave comporte six tons (sous la forme de cinq tons et deux demi-tons), et une tierce majeure comporte deux tons, on arrive donc à l'octave en ajoutant une tierce majeure à la dernière note de l'accord. ; Accords fondés sur une tierce mineure * accord diminué (la quinte est diminuée) : dim, o, ♭5 *: « ''do'' dim », « ''do''<sup>o</sup> », « ''do''<sup>♭5</sup> », « Cdim », « C<sup>o</sup> » ou « C<sup>♭5</sup> » pour l'accord de ''do'' diminuné (''do'' - ''mi''♭ - ''sol''♭) : On remarque que la quinte diminuée est l'enharmonique de la quarte augmentée et est l'intervalle appelé « triton » (car composé de trois tons). ; Accords fondés sur une tierce majeure ou mineure * accord suspendu de seconde : la tierce est remplacée par une seconde majeure : sus2 *: « ''do''<sup>sus2</sup> » ou « C<sup>sus2</sup> » pour l'accord de ''do'' majeur suspendu de seconde (''do''-''ré''-''sol'') * accord suspendu de quarte : la tierce est remplacée par une quarte juste : sus4 *: « ''do''<sup>sus4</sup> » ou « C<sup>sus4</sup> » pour l'accord de ''do'' majeur suspendu de quarte (''do''-''fa''-''sol'') ==== Triades appauvries ==== ; Accords fondés sur une tierce majeure ou mineure * accord de puissance : la tierce est omise, l'accord n'est constitué que de la fondamentale et de la quinte juste : 5 *: « ''do''<sup>5</sup> », « C<sup>5</sup> » pour l'accord de puissance de ''do'' (''do'' - ''la'') {{note|Très utilisé dans les musiques rock, hard rock et heavy metal, il est souvent joué renversé (''la'' - ''do'') ou bien avec l'ajout de l'octave (''do'' - ''la'' - ''do'').}} ==== Triades enrichies ==== ; Accords fondés sur une tierce majeure * accord de septième (la 7<sup>e</sup> est mineure) : 7 *: « ''do''<sup>7</sup> », « C<sup>7</sup> » pour l'accord de ''do'' septième, appelé « accord de septième de dominante de ''fa'' majeur » en musique classique (''do'' - ''mi'' - ''sol'' - ''si''♭) * accord de septième majeure : Δ, 7M ou maj7 *: « ''do'' <sup>Δ</sup> », « ''do'' <sup>maj7</sup> », « C<sup>Δ</sup> », « C<sup>7M</sup> »… pour l'accord de ''do'' septième majeure (''do'' - ''mi'' - ''sol'' - ''si'') ; Accords fondés sur une tierce mineure * accord de mineur septième (la tierce et la 7<sup>e</sup> sont mineures) : m7, min7 ou –7 *: « ''do'' m<sup>7</sup> », « ''do'' –<sup>7</sup> », « Cm<sup>7</sup> », « C–<sup>7</sup> »… pour l'accord de ''do'' mineur septième, appelé « accord de septième de dominante de ''fa'' mineur » en musique classique (''do'' - ''mi''♭ - ''sol'' - ''si''♭) * accord mineure septième majeure : m7M, m7maj, mΔ, –7M, –7maj, –Δ *: « ''do'' m<sup>7M</sup> », « ''do'' m<sup>maj7</sup> », « ''do'' –<sup>Δ</sup> », « Cm<sup>7M</sup> », « Cm<sup>maj7</sup> », « C–<sup>Δ</sup> »… pour l'accord de ''do'' mineur septième majeure (''do'' - ''mi''♭ - ''sol'' - ''si'') * accord de septième diminué (la quinte et la septième sont diminuée) : dim 7 ou o7 *: « ''do'' dim<sup>7</sup> », « ''do''<sup>o7</sup> », « Cdim<sup>7</sup> » ou « C<sup>o7</sup> » pour l'accord de ''do'' septième diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si''♭) * accord demi-diminué (seule la quinte est diminuée, la septième est mineure) : Ø ou –7(♭5) *: « ''do''<sup>Ø</sup> », « ''do''<sup>7(♭5)</sup> », « C<sup>Ø</sup> » ou « C<sup>7♭5</sup> » pour l'accord de ''do'' demi-diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si'') === Construction pythagoricienne des accords === Nous avons vu au débuts que lorsque l'on joue deux notes en même temps, leurs vibrations se superposent. Certaines superpositions créent un phénomène de battement désagréable, c'est le cas des secondes. Dans le cas d'une tierce majeure, les fréquences des notes quadruple et quintuple d'une même base : les fréquences s'écrivent 4׃<sub>0</sub> et 5׃<sub>0</sub>. Cette superposition de vibrations est agréable à l'oreille. Nous avons également vu que dans le cas d'une quinte juste, les fréquences sont le double et le triple d'une même base, ou encore le quadruple et sextuple si l'on considère la moitié de cette base. Ainsi, dans un accord parfait majeur, les fréquences des fondamentales des notes sont dans un rapport 4, 5, 6. De même, dans le cas d'un accord parfait mineur, les proportions sont de 1/6, 1/5 et 1/4. {{voir|[[../Caractéristiques_et_notation_des_sons_musicaux#Construction_pythagoricienne_et_gamme_de_sept_tons|Caractéristiques et notation des sons musicaux &gt; Construction pythagoricienne et gamme de sept tons]]}} === Un peu de physique : interférences === Les sons sont des vibrations. Lorsque l'on émet deux sons ou plus simultanément, les vibrations se superposent, on parle en physique « d'interférences ». Le modèle le plus simple pour décrire une vibration est la [[w:fr:Fonction sinus|fonction sinus]] : la pression de l'air P varie en fonction du temps ''t'' (en secondes, s), et l'on a pour un son « pur » : : P(''t'') ≈ sin(2π⋅ƒ⋅''t'') où ƒ est la fréquence (en hertz, Hz) du son. Si l'on émet deux sons de fréquence respective ƒ<sub>1</sub> et ƒ<sub>2</sub>, alors la pression vaut : : P(''t'') ≈ sin(2π⋅ƒ<sub>1</sub>⋅''t'') + sin(2π⋅ƒ<sub>2</sub>⋅''t''). Nous avons ici une [[w:fr:Identité trigonométrique#Transformation_de_sommes_en_produits,_ou_antilinéarisation|identité trigonométrique]] dite « antilinéarisation » : : <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{f_1 + f_2}{2}t \right ) \cdot \sin \left ( 2\pi \frac{f_1 - f_2}{2}t \right ).</math> On peut étudier simplement deux situations simples. [[Fichier:Battements interferentiels.png|vignette|Deux sons de fréquences proches créent des battements : la superposition d'une fréquence et d'une enveloppe.]] La première, c'est quand les fréquences ƒ<sub>1</sub> et ƒ<sub>2</sub> sont très proches. Alors, la moyenne (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2 est très proche de ƒ<sub>1</sub> et ƒ<sub>2</sub> ; et la demie différence (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2 est très proche de zéro. On a donc une enveloppe de fréquence très faible, (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2, dans laquelle s'inscrit un son de fréquence moyenne, (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2. C'est cette enveloppe de fréquence très faible qui crée les battements, désagréables à l'oreille. Sur l'image ci-contre, le premier trait rouge montre un instant où les vibrations sont opposées ; elles s'annulent, le son s'éteint. Le second trait rouge montre un instant où les vibrations sont en phase : elle s'ajoutent, le son est au plus fort. {{clear}} La seconde, c'est lorsque les deux fréquences sont des multiples entiers d'une même fréquence fondamentale ƒ<sub>0</sub> : ƒ<sub>1</sub> = ''n''<sub>1</sub>⋅ƒ<sub>0</sub> et ƒ<sub>0</sub> = ''n''<sub>0</sub>⋅ƒ<sub>0</sub>. On a alors : : <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{n_1 + n_2}{2}f_0 \cdot t \right ) \cdot \sin \left ( 2\pi \frac{n_1 - n_2}{2}f_0 \cdot t \right ).</math> On multiplie donc deux fonctions qui ont des fréquences multiples de ƒ<sub>0</sub>. La différence minimale entre ''n''<sub>1</sub> et ''n''<sub>2</sub> vaut 1 ; on a donc une enveloppe dont la fréquence est au minimum la moitié de ƒ<sub>0</sub>, c'est-à-dire un son une octave en dessous de ƒ<sub>0</sub>. Donc, cette enveloppe ne crée pas d'effet de battement, ou plutôt, le battement est trop rapide pour être perçu comme tel. Dans cette enveloppe, on a une fonction sinus dont la fréquence est également un multiple de ƒ<sub>0</sub> ; l'enveloppe et la fonction qui y est inscrite ont donc de nombreux « points communs », d'où l'effet harmonieux. === Le tonnetz === [[File:Speculum musicae.png|thumb|right|225px|Euler, ''De harmoniæ veris principiis'', 1774, p. 350.]] En allemand, le terme ''Tonnetz'' (se prononce « tône-netz ») signifie « réseau tonal ». C'est une représentation graphique des notes qui a été imaginée par [[w:Leonhard Euler|Leonhard Euler]] en 1739. Cette représentation graphique peut aider à la mémorisation de certains concepts de l'harmonie. Cependant, son application est très limitée : elle ne concerne que l'intonation juste d'une part, et que les accords parfait des tonalités majeures et mineures naturelles d'autre part. La représentation contenant les douze notes de la musique savante occidentale, on peut bien sûr représenter d'autres objets, comme les accords de septième ou les accords diminués, mais la représentation graphique est alors compliquée et perd son intérêt pédagogique. On part d'une note, par exemple le ''do''. Si on progresse vers la droite, on monte d'une quinte juste, donc ''sol'' ; vers la gauche, on descend d'une quinte juste, donc ''fa''. Si on va vers le bas, on monte d'une tierce majeure, donc ''mi'' ; si on va vers le haut, on descend d'une tierce majeure, donc ''la''♭ ou ''sol''♯ fa — do — sol — ré | | | | la — mi — si — fa♯ | | | | do♯ — sol♯ — ré♯ — si♭ La figure forme donc un filet, un réseau. On voit que ce réseau « boucle » : si on descend depuis le ''do''♯, on monte d'une tierce majeure, on obtient un ''mi''♯ qui est l'enharmonique du ''fa'' qui est en haut de la colonne. Si on va vers la droite à partir du ''ré'', on obtient le ''la'' qui est au début de la ligne suivante. Si on ajoute des diagonales allant vers la droite et le haut « / », on met en évidence des tierces mineures : ''la'' - ''do'', ''mi'' - ''sol'', ''si'' - ''ré'', ''do''♯ - ''mi''… fa — do — sol — ré | / | / | / | la — mi — si — fa♯ | / | / | / | do♯ — sol♯ — ré♯ — si♭ Donc les liens représentent : * | : tierce majeure ; * — : quinte juste ; * / : tierce mineure. [[Fichier:Tonnetz carre accords fr.svg|thumb|Tonnetz avec les accords parfaits. Les notes sont en notation italienne et les accords en notation jazz.]] On met ainsi en évidence des triangles dont un côté est une quinte juste, un côté une tierce majeure et un côté une tierce mineure ; c'est-à-dire que les notes aux sommets du triangle forment un accord parfait majeur (par exemple ''do'' - ''mi'' - ''sol'') : <div style="font-family:courier; background-color:#fafafa"> fa&nbsp;&nbsp;—&nbsp;'''do&nbsp;&nbsp;&nbsp;—&nbsp;sol'''&nbsp;—&nbsp;ré<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;'''|&nbsp;&nbsp;&nbsp;/'''&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;|<br /> la&nbsp;&nbsp;—&nbsp;'''mi'''&nbsp;&nbsp;&nbsp;—&nbsp;si&nbsp;&nbsp;—&nbsp;fa♯<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;/&nbsp;&nbsp;|<br /> do♯&nbsp;—&nbsp;sol♯&nbsp;—&nbsp;ré♯&nbsp;—&nbsp;si♭ </div> ou un accord parfait mineur (''la'' - ''do'' - ''mi''). <div style="font-family:courier; background-color:#fafafa"> fa&nbsp;&nbsp;—&nbsp;'''do'''&nbsp;&nbsp;&nbsp;—&nbsp;sol&nbsp;—&nbsp;ré<br /> |&nbsp;&nbsp;'''/&nbsp;&nbsp;|'''&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;|<br /> '''la&nbsp;&nbsp;—&nbsp;mi'''&nbsp;&nbsp;&nbsp;—&nbsp;si&nbsp;&nbsp;—&nbsp;fa♯<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;/&nbsp;&nbsp;|<br /> do♯&nbsp;—&nbsp;sol♯&nbsp;—&nbsp;ré♯&nbsp;—&nbsp;si♭ </div> Un triangle représente donc un accord, et un sommet représente une note. Si on passe d'un triangle à un triangle voisin, alors on passe d'un accord à un autre accord, les deux accords ayant deux notes en commun. Ceci illustre la notion de « plus court chemin » en harmonie : si on passe d'un accord à un autre en gardant un côté commun, alors on a un mouvement conjoint sur une seule des trois voix. Par rapport à l'harmonie fonctionnelle : les accords sont contigus à leur fonction, par exemple en ''do'' majeur : * fonction de tonique ({{Times New Roman|I}}) : C, A– et E– sont contigus ; * fonction de sous-dominante ({{Times New Roman|IV}}) : F et D– sont contigus ; * fonction de dominante ({{Times New Roman|V}}) : G et B<sup>o</sup> sont contigus. On notera que les triangles d'un schéma ''tonnetz'' ne représentent que des accords parfaits. Pour représenter un accord de quinte diminuée (''si'' - ''ré'' - ''fa'') ou les accords de septième, en particulier l'accord de septième de dominante, il faut étendre le ''tonnetz'' et l'on obtient des figures différentes. Par ailleurs, il est adapté à ce que l'on appelle « l'intonation juste », puisque tous les intervalles sont idéaux. [[Fichier:Tonnetz carre accords etendu fr.svg|vignette|Tonnetz étendu.]] [[Fichier:Tonnetz carre do majeur accords fr.svg|vignette|Tonnetz de la tonalité de ''do'' majeur. La représentation de l'accord de quinte diminuée sur ''si'' (B<sup>o</sup>) est une ligne et non un triangle.]] [[Fichier:Tonnetz carre do mineur accords fr.svg|vignette|Tonnetz des tonalités de ''do'' mineur naturel (haut) et ''do'' mineur harmonique (bas).]] Si l'on étend un peu le réseau : ré♭ — la♭ — mi♭ — si♭ — fa | / | / | / | / | fa — do — sol — ré — la | / | / | / | / | la — mi — si — fa♯ — do♯ | / | / | / | / | do♯ — sol♯ — ré♯ — la♯ — mi♯ | / | / | / | / | mi♯ — do — sol — ré — la on peut donc trouver des chemins permettant de représenter les accords de septième de dominante (par exemple en ''do'' majeur, G<sup>7</sup>) fa / sol — ré | / si et des accords de quinte diminuée (en ''do'' majeur : B<sup>o</sup>) fa / ré / si Une gamme majeure ou mineure naturelle peut se représenter par un trapèze rectangle : ''do'' majeur fa — do — sol — ré | / la — mi — si et ''do'' mineur la♭ — mi♭ — si♭ / | fa — do — sol — ré En revanche, la représentation d'une tonalité nécessite d'étendre le réseau afin de pouvoir faire figurer tous les accords, deux notes sont représentées deux fois. La représentation des tonalités mineures harmoniques prend une forme biscornue, ce qui nuit à l'intérêt pédagogique de la représentation. [[Fichier:Neo-Riemannian Tonnetz.svg|vignette|upright=2|Tonnetz avec des triangles équilatéraux.]] On peut réorganiser le schéma en décalant les lignes, afin d'avoir des triangles équilatéraux. Sur la figure ci-contre (en notation anglo-saxonne) : * si on monte en allant vers la droite « / », on a une tierce mineure ; * si on descend en allant vers la droite « \ », on a une tierce majeure ; * les liens horizontaux « — » représentent toujours des quintes justes * les triangles pointe en haut sont des accords parfaits mineurs ; * les triangles pointe en bas sont des accords parfaits majeurs. On a alors les accords de septième de dominante F / G — D \ / B et de quinte diminuée F / D / B les tonalités majeures F — C — G — D \ / A — E — B et les tonalités mineures naturelles A♭ — E♭ — B♭ / \ F — C — G — D == Notes et références == {{références}} == Voir aussi == === Bibliographie === * {{ouvrage |prénom1=Francesco |nom1=Gasparini |titre=L'Armonico Pratico al Cimbalo |éditeur=Giusseppe Antonio Silvani |lieu=Bologne |année=1772 |lang=it |url=https://imslp.org/wiki/L'Armonico_Pratico_al_Cimbalo_(Gasparini%2C_Francesco)}} === Liens externes === {{wikipédia|Consonance (harmonie tonale)}} {{wikipédia|Disposition de l'accord}} {{wikisource|Petit Manuel d’harmonie}} * {{lien web | url = https://www.apprendrelesolfege.com/chiffrage-d-accords | titre = Chiffrage d'accords (classique) | site = Apprendrelesolfege.com | consulté le = 2020-12-03 }} * {{lien web | url = https://www.coursd-harmonie.fr/introduction/introduction2_le_chiffrage_d_accords.php | titre = Introduction II : Le chiffrage d'accords | site = Cours d'harmonie.fr | consulté le = 2021-12-14 }} * {{lien web | url=https://www.coursd-harmonie.fr/ | titre = Cours d'harmonie en ligne | auteur = Jean-Baptiste Voinet | site=coursd-harmonie.fr | consulté le = 2021-12-20 }} * {{lien web | url=http://e-harmonie.e-monsite.com/ | titre = Cours d'harmonie classique en ligne | auteur = Olivier Miquel | site=e-harmonie | consulté le = 2021-12-24 }} * {{lien web | url=https://fr.audiofanzine.com/theorie-musicale/editorial/dossiers/les-gammes-et-les-modes.html | titre = Les bases de l’harmonie | site = AudioFanzine | date = 2013-07-23 | consulté le = 2024-01-12 }} * {{lien web | url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/l-accord-de-tristan-dechiffre-et-desacralise-2481605 | titre = L’Accord de Tristan déchiffré et désacralisé ! | site = France Musique | date = 2025-10-22 | consulté le = 2025-10-23 }} * {{lien web | url=https://www.youtube.com/watch?v=X62SAnyfQYw | titre = Qu'est-ce que la basse continue dans la musique baroque ? | auteur = Clément Rochefort | site = YouTube/France Musique | date = 2022-05-02 | consulté le = 2026-08-04 }} ---- ''[[../Mélodie|Mélodie]]'' &lt; [[../|↑]] &gt; ''[[../Représentation musicale|Représentation musicale]]'' [[Catégorie:Formation musicale (livre)|Harmonie]] rn3ejc31txbcvj5g0xez1jhym6pobsl 773910 773909 2026-10-02T08:45:43Z Cdang 1202 /* Les accords en général */ voc. 773910 wikitext text/x-wiki {{Bases de solfège}} <span style="font-size:25px;">6. Harmonie</span> L'harmonie désigne les notes jouées en même temps, soit plusieurs instruments jouant chacun une note, soit un instrument jouant un accord (instrument dit polyphonique). == Première approche == L'exemple le plus simple d'harmonie est sans doute la chanson en canon : c'est un chant polyphonique, c'est-à-dire à plusieurs voix, chaque voix chantant la même chose en décalé. Prenons par exemple ''Vent frais, vent du matin'' (la version originale est ''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609) : [[Fichier:Vent frais vent du matin.svg|class=transparent|center|Partition de ''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]] [[Fichier:Vent frais vent du matin.midi|vignette|''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]] nous voyons que les voix se superposent de manière « harmonieuse ». Les notes de chaque voix se correspondent point par point (avec un retard), c'est donc un type d'harmonie polyphonique appelé « contrepoint ». Considérons la première note de la mesure 6 pour chaque voix. Nous avons la superposition des notes ''ré''-''fa''-''la'' (du grave vers l'aigu) ; la superposition de notes jouées ou chantées ensembles s'appelle un accord. Cet accord ''ré''-''fa''-''la'' porte le nom « d'accord parfait de ''ré'' mineur » : * « ''ré'' » car la note fondamentale est un ''ré'' ; * « parfait » car il est l'association d'une tierce, ''ré''-''fa'', et d'une quinte juste, ''ré''-''la'' ; * « mineur » car le premier intervalle, ''ré''-''fa'', est une tierce mineure. Considérons maintenant un chant accompagné au piano. Le piano peut jouer plusieurs notes en même temps, il peut jouer des accords. [[Fichier:Au clair de le lune chant et piano.svg|class=transparent|center|Deux premières mesure d’Au clair de la lune.]] [[Fichier:Au clair de le lune chant et piano.midi|vignette|Deux premières mesure d’Au clair de la lune.]] L'accord, les notes à jouer simultanément, sont écrites « en colonne ». Lorsqu'on les énonce, on les lit de bas en haut mais le pianiste les joue en pressant les touches du clavier en même temps, de manière « plaquée ». Le premier accord est composé des notes ''do''-''mi''-''sol'' ; il est appelé « accord parfait de ''do'' majeur » car la note fondamentale est ''do'', qu'il est l'association d'une tierce et d'une quinte juste et que le premier intervalle, ''do''-''mi'', est une tierce majeure. Si l'on lit toutes les notes, de la plus grave à la plus aiguë, nous avons : ''do'' (à la clef de ''fa'', main gauche du piano) - ''do'' - ''mi'' - ''sol'' (main droite du piano) - ''do'' (chant) ; nous voyons que la note ''do'' est jouée plusieurs fois, cela ne change pas la qualité de l'accord. == Consonance et dissonance == [[Fichier:Seconde majeure septieme mineure.svg|vignette|Seconde majeure et son renversement, le septième mineure.]] Les notions de consonance et de dissonance sont culturelles et changent selon l'époque. Nous pouvons néanmoins noter que : * l'accord de seconde, et son renversement la septième, créent des battements, les notes « frottent », c'est un intervalle harmonique dissonant ; mais dans le cas de la septième, comme les notes sont éloignées, le frottement est moins perceptible ; * les accords d'unisson, de tierce, quarte et quinte sonnent agréablement à l'oreille, ils sont consonants. Dans la musique savante européenne, au début du Moyen-Âge, seuls les accords d'unisson, de quarte et de quinte étaient considérés comme consonants, d'où leur qualification de « juste ». La tierce, et son renversement la sixte, étaient perçues comme dissonantes ; à partir de la Renaissance, ces deux intervalles sont considérés comme consonants. L'harmonie joue avec les consonances et les dissonances. Dans un premier temps, les harmonies dissonantes sont utilisées pour créer des tensions qui sont ensuite résolues, on utilise des successions « consonant-dissonant-consonant ». À force d'entendre des intervalles considérés comme dissonants, l'oreille s'habitue et certains finissent par être considérés comme consonants ; c'est ce qui est arrivé à la tierce et à la sixte à la fin du Moyen Âge avec le contrepoint, puis à la septième plus tardivement. Il faut ici aborder la notion d'harmonique des notes. [[File:Harmoniques de do.svg|thumb|Les six premières harmoniques de ''do''.]] Lorsque l'on joue une note, on entend d'autres notes plus aigües et plus faibles ; la note jouée est appelée la « fondamentale » et les notes plus aigües et plus faibles sont les « harmoniques ». C'est cette accumulation d'harmoniques qui donne la couleur au son, son timbre, qui fait qu'un piano ne sonne pas comme un violon. Par exemple, si l'on joue un ''do''<sup>1</sup><ref>Pour la notation des octaves, voir ''[[../Représentation_musicale#Désignation_des_octaves|Représentation musicale &gt; Désignation des octaves]]''.</ref> (fondamentale), on entend le ''do''<sup>2</sup> (une octave plus aigu), puis un ''sol''<sup>2</sup>, puis encore un ''do''<sup>3</sup> plus aigu, puis un ''mi''<sup>3</sup>, puis encore un ''sol''<sup>3</sup>, puis un ''si''♭<sup>3</sup>… Ainsi, puisque lorsque l'on joue un ''do'' on entend aussi un ''sol'' très léger, alors jouer un ''do'' et un ''sol'' simultanément n'est pas choquant. De même pour ''do'' et ''mi''. De là vient la notion de consonance. Le statut du ''si''♭ est plus ambigu. Il fait partie des harmoniques qui sonnent naturellement, mais il forme une seconde descendante avec le ''do'', intervalle dissonant. Par ailleurs, on remarque que le ''si''♭ ne fait pas partie de la gamme de ''do'' majeur, contrairement au ''sol'' et au ''mi''. Pour le jeu sur les dissonances, on peut écouter par exemple la ''Toccata'' en ''ré'' mineur, op. 11 de Sergueï Prokofiev (1912). : {{lien web |url=https://www.youtube.com/watch?v=AVpnr8dI_50 |titre=Yuja Wang Prokofiev Toccata |site=YouTube |date=2019-02-26 |consulté le=2021-12-19}} == Contrepoint == Dans le chant grégorien, la notion d'accord n'existe pas. L'harmonie provient de la superposition de plusieurs mélodies, notamment dans ce que l'on appelle le « contrepoint ». Le terme provient du latin ''« punctum contra punctum »'', littéralement « point par point », et désigne le fait que les notes de chaque voix se correspondent. L'exemple le plus connu de contrepoint est le canon, comme ''Frère Jacques'' ou bien ''Vent frais, vent du matin'' présenté ci-dessus : chaque note d'un couplet correspond à une note du couplet précédent. Certains morceaux sont bâtis sur une écriture « en miroir » : l'ordre des notes est inversé entre les deux voix, ou bien les intervalles sont inversés (« mouvement contraire » : une tierce montante sur une voix correspond à une tierce descendante sur l'autre). On peut également citer le « mouvement oblique » (une des voix, le bourdon, chante toujours la même note) et le mouvement parallèle (les deux voix chantent le même air mais transposé, l'une est plus aiguë que l'autre). Nous reproduisons ci-dessous le début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel. [[Fichier:Haendel Sonate en trio re mineur debut canon.svg | vignette | center | upright=2 | Début du second ''Allergo'' de la sonate en trio en ''ré'' mineur de Haendel.]] [[Fichier:Haendel Sonate en trio re mineur debut.midi | vignette | Début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.]] Nous avons mis en évidence la construction en canon avec des encadrés de couleur : sur les quatre premières mesures, nous voyons trois motifs repris alternativement par une voix et par l'autre. Ce type de procédé est très courant dans la musique baroque. Les procédés du contrepoint s'appliquent également à la danse : * unisson : les danseurs et danseuses font les mêmes gestes en même temps ; * répétition : le fait de répéter une série de gestes, une « phrase dansante » ; * canon : les gestes sont faits avec un décalage régulier d'un danseur ou d'une danseuse à l'autre ; * cascade : forme de canon dans laquelle le décalage est très petit ; * contraste : deux danseur·euses, ou deux groupes, ont des gestuelles très différentes ; * accumulation : la gestuelle se complexifie par l'ajout d'éléments au fur et à mesure ; ou bien le nombre de danseur·euses augmente ; * dialogue : les gestes de danseur·euses ou de groupes se répondent ; * contre-point : la gestuelle d'un ou une danseuse se superpose à la gestuelle d'un groupe ; * lâcher-rattraper : les danseurs et danseuses alternent danse à l'unisson et gestuelles indépendantes. : {{lien web | url=https://www.youtube.com/watch?v=wgblAOzedFc | titre=Les procédés de composition en danse | auteur= Doisneau Sport TV | site=YouTube | date=2020-03-16 | consulté le=2021-01-21 }} {{...}} == Ostinato == L'ostinato, mot italien signifiant « obstiné », c'est le fait de jouer en boucle un passage. L'ostinato peut ainsi former une structure autour de laquelle peut se développer le morceau. Lorsque l'ostinato est joué par l'instrument de basse (basson, violoncelle, viole de gambe basse, flûte à bec basse, main gauche du piano…), on parle de basse obstinée. Les grilles d'accord en jazz sont des ostinatos (ou ostinati) joués par la section rythmique. Exemples : * {{lien web |url=https://www.youtube.com/watch?v=QZpGe5rNJkI&t=41s |titre=Epic Patty Cake Song (I'll Think Of You) |auteur= Kurt Hugo Schneider |site=YouTube |date=2014-08-12 |consulté le=2026-01-24}} : un ostinato de percussions corporelles ; * {{lien web |url=https://www.youtube.com/watch?v=Kv-37-nXC48 |titre=Pow Wow ''Le lion est mort ce soir'' Les Victoires de la Musique 1993 |auteur=Solomon Linda |date=2015-02-02 |consulté le=2026-01-24}} : dans ce chant ''a capella'' (sans accompagnement d'instrument), les voix de basse forment une basse obstinée, sur les syllabes « oëap » durant le couplet et « awimbowé » sur le refrain ; * {{lien web |url=https://www.youtube.com/watch?v=JvNQLJ1_HQ0 |titre=Pachelbel Canon in D Major - the original and best version. |auteur=Voices of Music |site=YouTube |date=2008-09-02 |consulté le=2026-01-24}} : le ''Canon de Pachelbel de ''ré'' mineur'' (''Canon per tre Violini e Basso'', Johann Pachelbel, vers 1680) est construit sur une basse obstinée de huit mesures. == Les accords en général == Initialement, on a des chants polyphoniques, des voix qui chantent chacune une mélodie, les mélodies se mêlant. On remarque que certaines superpositions de notes sonnent de manière plus ou moins agréables, consonantes ou dissonantes. On en vient alors à associer ces notes, c'est-à-dire à considérer dès le départ la superposition de ces notes et non pas la rencontre de ces notes au gré des mélodies. Ces groupes de notes superposées forment les accords. En Europe, cette notion apparaît vers le {{pc|xiv}}<sup>e</sup> siècle avec notamment la ''[[wikipedia:fr:Messe de Notre Dame|Messe de Notre Dame]]'' de Guillaume de Machaut (vers 1360-1365). À l'origine, dans les chants polyphoniques, les voix sont d'égale importance ; pour un chant à trois voix, les voix sont appelées, de la plus grave à la plus aigüe, « basse », « taille » (ou ténor) et « dessus » (ou soprano). Puis, au cours du temps, une des voix devient plus importante, le « cantus firmus » ; initialement, il s'agit de la voix intermédiaire, la taille (ténor), puis c'est la voix la plus aigüe, le dessus (soprano). À la Renaissance, au {{pc|xvi}}{{e}} siècle, apparaît la notion de « monodie accompagnée » : la voix principale, le cantus firmus, devient la voix mélodique, la monodie, et les autres voix servent à l'accompagner. À l'époque baroque, au {{pc|xvii}}{{e}} siècle, apparaît la notion d'accord, notes sonnant simultanément, avec une notation simplifiée, la « basse chiffrée » : a lieu d'écrire toutes les notes de l'accord, on écrit la basse et des chiffres indiquent quelles sont les autres notes à jouer. La notion « d'accord parfait » est consacrée par [[wikipedia:fr:Jean-Philippe Rameau|Jean-Philippe Rameau]] dans son ''Traité de l'harmonie réduite à ses principes naturels'', publié en 1722. === Qu'est-ce qu'un accord ? === Un accord est un ensemble d'au minimum trois notes jouées en même temps. « Jouées » signifie qu'il faut qu'à un moment donné, elles sonnent en même temps, mais le début ou la fin des notes peut être à des instants différents. Considérons que l'on joue les notes ''do'', ''mi'' et ''sol'' en même temps. Cet accord s'appelle « accord de ''do'' majeur ». En musique classique, on lui adjoint l'adjectif « parfait » : « accord parfait de ''do'' majeur ». Nous représentons ci-dessous trois manière de faire l'accord : avec trois instruments jouant chacun une note : [[Fichier:Do majeur trois portees.svg|class=transparent|center|Accord de ''do'' majeur avec trois instruments différents.]] Avec un seul instrument jouant simultanément les trois notes : [[Fichier:Chord C.svg|class=transparent|center|Accord de ''do'' majeur joué par un seul instrument.]] L'accord tel qu'il est joué habituellement par une guitare d'accompagnement : [[Fichier:Do majeur guitare.svg|class=transparent|center|Accord de ''do'' majeur à la guitare.]] Pour ce dernier, nous représentons le diagramme indiquant la position des doigts sur le manche au dessus de la portée et la tablature en dessous. Ici, c'est au total six notes qui sont jouées : ''mi'' grave, ''do'' médium, ''mi'' médium, ''sol'' médium, ''do'' aigu, ''mi'' aigu. Mais il s'agit bien des trois notes ''do'', ''mi'' et ''sol'' jouées à des octaves différentes. Nous remarquons également que la note de basse (la note la plus grave), ''mi'', est différente de la note fondamentale (celle qui donne le nom à l'accord), ''do'' ; l'accord est dit « renversé » (voir plus loin). === Comment joue-t-on un accord ? === Les notes ne sont pas forcément jouées en même temps ; elles peuvent être « égrainées », jouée successivement, ce que l'on appelle un arpège. La partition ci-dessous montre six manières différentes de jouer un accord de ''la'' mineur à la guitare, plaqué puis arpégé. [[Fichier:La mineur differentes executions.svg|class=transparent|center|Différentes exécution de l'accord de do majeur à la guitare.]] [[Fichier:La mineur differentes executions midi.midi|vignette|Différentes exécution de l'accord de la mineur à la guitare.]] Vous pouvez écouter l'exécution de cette partition avec le lecteur ci-contre. Seuls les instruments polyphoniques peuvent jouer les accords plaqués : instruments à clavier (clavecin, orgue, piano, accordéon), les instruments à plusieurs cordes pincées (harpe, guitare ; violon, alto, violoncelle et contrebasse joués en pizzicati). Les instruments à corde frottés de la famille du violon peuvent jouer des notes par deux à l'archet mais pas plus du fait de la forme bombée du chevalet ; cependant, un mouvement rapide permet de jouer les quatre cordes de manière très rapprochée. Les instruments à percussion de type xylophone ou le tympanon permettent de jouer jusqu'à quatre notes simultanément en tenant deux baguettes (mailloches, maillets) par main. Tous les instruments peuvent jouer des arpèges même si, dans le cas des instruments monodiques, les notes ne continuent pas à sonner lorsque l'on passe à la note suivante. L'arpège peut être joué par l'instrument de basse (basson, violoncelle, contrebasse, guitare basse, pédalier de l'orgue…), notamment dans le cas d'une basse continue ou d'une ''{{lang|en|walking bass}}'' (« basse marchante » : la basse joue des noires, donnant ainsi l'impression qu'elle marche). En jazz, et spécifiquement au piano, on a recours au ''{{lang|en|voicing}}'' : on choisit la manière dont on organise les notes pour donner une couleur spécifique, ou bien pour créer une mélodie en enchaînant les accords. Il est fréquent de ne pas jouer toutes les notes : si on n'en garde que deux, ce sont la tierce et la septième, car ce sont celles qui caractérisent l'accord (selon que la tierce est mineure ou majeure, que la septième est majeure ou mineure), et la fondamentale est en général jouée par la contrebasse ou guitare basse. {{clear}} === Classes d'accord === [[Fichier:Intervalles harmoniques accords classes.svg|vignette|upright=1.5|Intervalles harmoniques dans les accords classés de trois, quatre et cinq notes.]] Un accord composé d'empilement de tierces est appelé « accord classé ». En musique tonale, c'est-à-dire la musique fondée sur les gammes majeures ou mineures (cas majoritaire en musique classique), on distingue trois classes d'accords : * les accords de trois notes, ou triades, ou accords de quinte ; * les accords de quatre notes, ou accords de septième ; * les accords de cinq notes, ou accords de neuvième. En empilant des tierces, si l'on part de la note fondamentale, on a donc de intervalles de tierce, quinte, septième et neuvième. En musique tonale, les accords avec d'autres intervalles (hors renversement, voir ci-après), typiquement seconde, quarte ou sixte, sont considérés comme des transitions entre deux accords classés. Ils sont appelés, selon leur utilisation, « accords à retard » (en anglais : ''{{lang|en|suspended chord}}'', accord suspendu) ou « appoggiature » (note « appuyée », étrangère à l'harmonie). Voir aussi plus loin la notion de note étrangère. === Renversements d'accords === [[File:Accord do majeur renversements.svg|thumb|Accord parfait de do majeur et ses renversements.]] [[Fichier:Progression dominante renverse parfait do majeur.svg|vignette|upright=0.6|Progression accord de dominante renversé → accord parfait en ''do'' majeur.]] Un accord classé est donc un empilement de tierces. Si l'on change l'ordre des notes, on a toujours le même accord mais il est fait avec d'autres intervalles harmoniques. Par exemple, l'accord parfait de ''do'' majeur dans son état fondamental, c'est-à-dire non renversé, s'écrit ''do'' - ''mi'' - ''sol''. Sa note fondamentale, ''do'', est aussi se note de basse. Si maintenant on prend le ''do'' de l'octave supérieure, l'accord devient ''mi - sol - do'' ; c'est l'empilement d'une tierce ''(mi - sol)'' et d'une quarte ''(sol - do)'', soit la superposition d'une tierce ''(mi - sol)'' et d'une sixième ''(mi - do)''. C'est le premier renversement de l'accord parfait de ''do'' majeur ; la fondamentale est toujours ''do'' mais la basse est ''mi''. Le second renversement est ''sol - do - mi''. L'utilisation de renversement peut faciliter l'exécution de la progression d'accord. Par exemple, en tonalité ''do'' majeur, si l'on veut passer de l'accord de dominante ''sol - si - ré'' à l'accord parfait ''do - mi - sol'', alors on peut utiliser le second renversement de l'accord de dominante : ''ré - sol - si'' → ''do - mi - sol''. Ainsi, la basse descend juste d'un ton ''(ré → do)'' et sur un piano, la main reste globalement dans la même position. Le renversement d'un accord permet également de respecter certaines règles de l'harmonie classique, notamment éviter que des voix se suivent strictement (« mouvement parallèle »), ce qui aurait un effet de platitude. De manière générale, la notion de renversement permet deux choses : * d'enrichir l'œuvre : pour créer une harmonie donnée (c'est-à-dire des sons sonnant bien ensemble), nous avons plus de souplesse, nous pouvons organiser ces notes comme nous le voulons selon les voix ; * de simplifier l'analyse : quelle que soit la manière dont sont organisées les notes, cela nous ramène à un même accord. {{citation bloc|Or il, y a plusieurs manières de jouer un accord, selon que l'on aborde par la première note qui le constitue, ''do mi sol'', la deuxième, ''mi sol do'', ou la troisième note, ''sol do mi''. Ce sont les renversements, [que Rameau] va classer en différentes combinaisons d'une seule matrice. Faisant cela, Rameau divise le nombre d'accords [de septième] par quatre. Il simplifie, il structure […].|{{ouvrage|prénom1=André |nom1=Manoukian |titre=Sur les routes de la musique |éditeur=Harper Collins |année=2021 |passage=54 |isbn=979-1-03391201-9}} }} {{clear}} [[File:Plusieurs realisation 1er renversement doM.svg|thumb|Plusieurs réalisation du premier renversement de l'accord de ''do'' majeur.]] Notez que # Les notes peuvent être répétées plusieurs fois, à différentes octaves. # Dans la notion de renversement, seule importe en fait la note de basse. Ainsi, les accords ''mi-sol-do'', ''mi-do-sol'', ''mi-do-mi-sol'', ''mi-sol-mi-do''… sont tous une déclinaison du premier renversement de ''do-mi-sol'' et ils seront abrégés de la même manière (''mi''<sup>6</sup> en musique classique ou C/E en musique populaire et jazz, voir plus bas). {{clear}} == Notation des accords de trois notes == Les accords de trois notes sont appelés « accords de quinte » en classique, et « triades » en jazz. [[Fichier:Progression dominante renverse parfait do majeur chiffrage.svg|vignette|upright=0.7|Chiffrage du second renversement d'un accord de ''sol'' majeur et d'un accord de ''do'' majeur : notation en musique populaire et jazz (haut) et notation de basse chiffrée (bas).]] Les accords sont construits de manière systématique. Nous pouvons donc les représenter de manière simplifiée. Cette notation simplifiée des accords est appelée « chiffrage ». Reprenons la progression d'accords ci-dessus : « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » dans la tonalité de ''do'' majeur. On utilise en général trois notations différentes : * en musique populaire, jazz, rock… un accord est désigné par sa note fondamentale ; ici donc, les accords sont notés « ''sol'' - ''do'' » ou, en notation anglo-saxonne, « G - C » ;<br /> comme le premier accord est renversé, on indique la note de basse après une barre, la progression d'accords est donc chiffrée '''« ''sol''/''ré'' - ''do'' »''' ou '''« G/D - C »''' ;<br /> il s'agit ici d'accords composés d'une tierce majeure et d'une quinte juste ; si les accords sont constitués d'intervalles différents, nous ajoutons un symbole après la note : « m » ou « – » si la tierce est mineure, « dim » ou « ° » si la quinte est diminuée ; * en musique classique, on utilise la notation de « basse chiffrée » (utilisée notamment pour noter la basse continue en musique baroque) : on indique la note de basse sur la portée et on lui adjoint l'intervalle de la fondamentale à la note la plus haute (donc ici respectivement 6 et 5, puisque ''sol''-''si'' est une sixte et ''do''-''sol'' est une quinte), étant sous-entendu que l'on a des empilements de tierce en dessous ; mais dans le cas du premier accord, le premier intervalle n'est pas une tierce, mais une quarte ''(ré''-''sol)'', on note donc '''« ''ré'' <sup>6</sup><sub>4</sub> - ''do'' <sup>5</sup> »'''<ref>quand on ne dispose pas de la notation en supérieur (exposant) et inférieur (indice), on utilise parfois une notation sous forme de fraction : ''sol'' 6/4 et ''do'' 5/.</ref> ; * lorsque l'on fait l'analyse d'un morceau, on s'attache à identifier la note fondamentale de l'accord (qui est différente de la basse dans le cas d'un renversement) ; on indique alors le degré de la fondamentale : '''« {{Times New Roman|V<sup>6</sup><sub>4</sub> - I<sup>5</sup>}} »'''. La notation de basse chiffrée permet de construire l'accord à la volée : * on joue la note indiquée (basse) ; * s'il n'y a pas de 2 ni de 4, on lui ajoute la tierce ; * on ajoute les intervalles indiqués par le chiffrage. La notation de musique jazz oblige à connaître la composition des différents accords, mais une fois que ceux-ci sont acquis, il n'y a pas besoin de reconstruire l'accord. La notation de basse chiffrée avec les chiffres romains n'est pas utilisée pour jouer, mais uniquement pour analyser ; Sur les partitions avec basse chiffrée, il y a simplement les chiffrages indiqués au-dessus de la partie de basse. Le chiffrage avec le degré en chiffres romains présente l'avantage d'être indépendant de la tonalité et donc de se concentrer sur la fonction de l'accord au sein de la tonalité. Par exemple, ci-dessous, nous pouvons parler de la progression d'accords « {{Times New Roman|V - I}} » de manière générale, cette notation étant valable quelle que soit la tonalité. [[File:Progression dominante renverse parfait do majeur chiffrage basse continue.svg|thumb|Chiffrage en notation basse chiffrée de la progression d'accords « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » en do majeur.]] {{note|En notation de base continue avec fondamentale en chiffres romains, la fondamentale est toujours indiquée ''sous'' la portée de la partie de basse. Les intervalles sont indiqués au-dessus de la portée de la partie de basse ; lorsque l'on fait une analyse, on peut ayssi les indiquer à côté du degré en chiffres romains, donc sous la portée de la basse.}} {{note|En notation rock, le 5 en exposant indique un accord incomplet avec uniquement la fondamentale et la quinte, un accord sans tierce appelé « accord de puissance » ou ''{{lang|en|power chord}}''. Par exemple, C<sup>5</sup> est l'accord ''do-sol''.}} {{clear}} [[Fichier:Accords parfait do majeur basse chiffree fondamental et renverse.svg|vignette|upright=2.5|Chiffrage de l'accord parfait de ''do'' majeur en basse chiffrée, à l'état fondamental et ses renversements.]] Concernant les accords parfaits en notation de basse chiffrée : * un accord parfait à l'état fondamental est chiffré « <sup>5</sup> » ; on l'appelle « accord de quinte » ; * le premier renversement est chiffré « <sup>6</sup> » (la tierce est implicite) ; on l'appelle « accord de sixte » ; * le second renversement est noté « <sup>6</sup><sub>4</sub> » ; on l'appelle « accord de sixte et de quarte » (ou bien « de quarte et de sixte »). Par exemple, pour l'accord parfait de ''do'' majeur : * l'état fondamental ''do''-''mi''-''sol'' est noté ''do''<sup>5</sup> ; * le premier renversement ''mi''-''sol''-''do'' est noté ''mi''<sup>6</sup> ; * le second renversement ''sol''-''do''-''mi'' est noté ''sol''<sup>6</sup><sub>4</sub>. Il y a une exception : l'accord construit sur la sensible (7{{e}} degré) contient une quinte diminuée et non une quinte juste. Le chiffrage est donc différent : * l'état fondamental ''si''-''ré''-''fa'' est noté ''si''<sup><s>5</s></sup> (cinq barré), « accord de quinte diminuée » ; * le premier renversement ''ré''-''fa''-''si'' est noté ''ré''<sup>+6</sup><sub>3</sub>, « accord de sixte sensible et tierce » ; * le second renversement ''fa''-''si''-''ré'' est noté ''fa''<sup>6</sup><sub>+4</sub>, « accord de sixte et quarte sensible ». Par ailleurs, on ne considère pas qu'il est fondé sur la sensible, mais sur la dominante. Si certains indiquent le degré {{Times New Roman|VII}}, d'autres indiquent le degré {{Times New Roman|V}} en mettant des guillemets, « {{Times New Roman|“V”}} ». Donc selon l'état, le chiffrage est {{Times New Roman|“V”<sup><s>5</s></sup>}}, {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}} ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}. En notation jazz, on ajoute « dim », « <sup>o</sup> » ou bien « <sup>♭5</sup> » au chiffrage, ici : B dim, B<sup>o</sup> ou B<sup>♭5</sup> pour l'état fondamental. Pour les renversements : B dim/D et B dim/F ; ou bien B<sup>o</sup>/D et B<sup>o</sup>/F ; ou bien B<sup>♭5</sup>/D et B<sup>♭5</sup>/F. {{clear}} [[Fichier:Accords basse chiffree basse do fondamental et renverses.svg|vignette|upright=2|Basse chiffrée : accords de quinte, de sixte et de sixte et de quarte ayant pour basse ''do''.]] Et concernant les accords ayant pour basse ''do'' en tonalité de ''do'' majeur : * l'accord ''do''<sup>5</sup> est un accord à l'état fondamental, c'est donc l'accord ''do''-''mi''-''sol'' (sa fondamentale est ''do'') ; * l'accord ''do''<sup>6</sup> est le premier renversement d'un accord, c'est donc l'accord ''do''-''mi''-''la'' (sa fondamentale est ''la'') ; * l'accord ''do''<sup>6</sup><sub>4</sub> est le second renversement d'un accord, c'est donc l'accord ''do''-''fa''-''la'' (sa fondamentale est ''fa''). {{clear}} == Notes étrangères == La musique européenne s'appuie essentiellement sur des accords parfaits, c'est-à-dire fondés sur une tierce majeure ou mineure, et une quinte juste. Il arrive fréquemment qu'un accord ne soit pas un accord parfait. Les notes qui font partie de l'accord parfait sont appelées « notes naturelles » et la note qui n'en fait pas partie est appelée « note étrangère ». Il existe plusieurs types de notes étrangères : * anticipation : la note étrangère est une note naturelle de l'accord suivant ; * appogiature : note d'ornementation qui se résout par mouvement conjoint, c'est-à-dire qu'elle est suivie par une note située juste au-dessus ou en dessous (seconde ascendante ou descendante) qui est, elle, une note naturelle ; * broderie : on part d'une note naturelle, on monte ou on descend d'une seconde, puis on revient sur la note naturelle ; * double broderie : on part d'une note naturelle, on joue la note du dessus puis la note du dessous avant de revenir à la note naturelle ; ou bien on joue la note du dessous puis la note du dessus ; * échappée : note étrangère n'appartenant à aucune des autres catégories ; * note de passage : mouvement conjoint allant d'une note naturelle d'un accord à une note naturelle de l'accord suivant ; * pédale : la note de basse reste la même pendant plusieurs accords successifs ; * retard : la note étrangère est une note naturelle de l'accord précédent. Les notes étrangères ne sont pas chiffrées. [[File:Notes etrangeres accords.svg|center|Différents types de notes étrangères.]] {{note|Les anglophones distinguent deux types de retard : la ''{{lang|en|suspension}}'' est résolue vers le haut (le mouvement est ascendant), le ''{{lang|en|retardation}}'' est résolu vers le bas (le mouvement est descendant).}} == Principaux accords == Les trois principaux accords sont : * l'accord parfait majeur : il est construit sur les degrés {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (médiante) et {{Times New Roman|V}} (dominante) d'une gamme majeure ; il est noté {{Times New Roman|I}}<sup>5</sup>, {{Times New Roman|IV}}<sup>5</sup>, {{Times New Roman|V}}<sup>5</sup> ; * l'accord parfait mineur : il est construit sur les degrés {{Times New Roman|I}} (tonique) et {{Times New Roman|IV}} (sous-tonique) d'une gamme mineure harmonique ; il est également noté {{Times New Roman|I}}<sup>5</sup> et {{Times New Roman|IV}}<sup>5</sup>, les anglo-saxons le notent {{Times New Roman|i}}<sup>5</sup> et {{Times New Roman|iv}}<sup>5</sup> (la minuscule indiquant le caractère mineur) ; * l'accord de septième de dominante : il est construit sur le degré {{Times New Roman|V}} (dominante) d'une gamme majeure ou mineure harmonique ; il est noté {{Times New Roman|V}}<sup>7</sup><sub>+</sub>. On peut trouver ces trois accords sur d'autres degrés, et il existe d'autre types d'accords. Nous verrons cela plus loin. {| class="wikitable" |+ Constitution des principaux accords — Dénomination classique |- ! scope="col" | Accord ! scope="col" | 1<sup>er</sup> intervalle ! scope="col" | 2<sup>e</sup> intervalle ! scope="col" | 3<sup>e</sup> intervalle |- ! scope="row" | Accord parfait majeur | tierce majeure (3M) || quinte juste (5J) || — |- ! scope="row" | Accord parfait mineur | tierce mineure (3m) || quinte juste (5J) || — |- ! scope="row" | Accord de septième de dominante | tierce majeure (3M) || quinte juste (5J) || septième mineure (7m) |} {| class="wikitable" |+ Constitution des principaux accords — Dénomination jazz |- ! scope="col" | Accord ! scope="col" | 1<sup>er</sup> intervalle ! scope="col" | 2<sup>e</sup> intervalle ! scope="col" | 3<sup>e</sup> intervalle |- ! scope="row" | Triade majeure | tierce majeure (3M) || quinte juste (5J) || — |- ! scope="row" | Triade mineure | tierce mineure (3m) || quinte juste (5J) || — |- ! scope="row" | Accord de septième | tierce majeure (3M) || quinte juste (5J) || septième mineure (7m) |} {| border="0" |- | [[Fichier:Accord do majeur arpege puis plaque.midi | Accord parfait de ''do'' majeur (C).]] || [[Fichier:Accord do mineur arpege puis plaque.midi | Accord parfait de ''do'' mineur (Cm).]] || [[Fichier:Accord do septieme arpege puis plaque.midi | Accord de septième de dominante de ''fa'' majeur (C<sup>7</sup>).]] |- | Accord parfait<br /> de ''do'' majeur (C). || Accord parfait<br /> de ''do'' mineur (Cm). || Accord de septième de dominante<br /> de ''fa'' majeur (C<sup>7</sup>). |} '''Rappel :''' * la tierce mineure est composée d'un ton et demi (1 t ½) ; * la tierce majeur est composée de deux tons (2 t) ; * la quinte juste a la même altération que la fondamentale, sauf lorsque la fondamentale est ''si'' (la quinte juste est alors ''fa''♯) ; * la septième mineure est le renversement de la seconde majeure (1 t). [[File:Renversements accords pft fa maj basse chiffree.svg|thumb|Renversements de l'accord parfait de ''fa'' majeur, et la notation de basse chiffrée.]] [[File:Renversements accord sept de dom fa maj basse chiffree.svg|thumb|Renversements de l'accord de septième de dominante de ''fa'' majeur, et la notation de basse chiffrée.]] {| class="wikitable" |+ Notation des principaux accords en musique classique |- ! scope="col" | Accord ! scope="col" | État<br /> fondamental ! scope="col" | Premier<br /> renversement ! scope="col" | Deuxième<br /> renversement ! scope="col" | Troisième<br /> renversement |- ! scope="row" | Accord parfait | {{Times New Roman|I<sup>5</sup>}}<br/> acc. de quinte || {{Times New Roman|I<sup>6</sup>}}<br :> acc. de sixte || {{Times New Roman|I<sup>6</sup><sub>4</sub>}}<br /> acc. de quarte et de sixte || — |- ! scope="row" | Accord de septième<br /> de dominante | {{Times New Roman|V<sup>7</sup><sub>+</sub>}}<br /> acc.de septième de dominante || {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}<br />acc. de sixte et quinte diminuée || {{Times New Roman|V<sup>+6</sup>}}<br />acc. de sixte sensible || {{Times New Roman|V<sup>+4</sup>}}<br />acc. de quarte sensible<br />acc. de triton |} {| class="wikitable" |+ Notation des principaux accords en jazz |- ! scope="col" | Accord ! scope="col" | Chiffrage ! scope="col" | Renversements |- ! scope="row" | Triade majeure | X | rowspan="3" | Les renversements se notent en mettant la basse après une barre de fraction, par exemple pour la triade de ''do'' majeur : * état fondamental : C ; * premier renversement : C/E ; * second renversement : C/G. |- ! scope="row" | Triade mineure | Xm, X– |- ! scope="row" | Septième | X<sup>7</sup> |} {{clear}} Dans le cas d'un accord de septième de dominante, le nom de l'accord change selon que l'on est en musique classique ou en jazz : en musique classique, on donne le nom de la tonalité alors qu'en jazz, on donne le nom de la fondamentale. Ainsi, l'accord appelé « septième de dominante de ''do'' majeur » en musique classique, est appelé « ''sol'' sept » (G<sup>7</sup>) en jazz : la dominante (degré {{Times New Roman|V}}, dominante) de la tonalité de ''do'' majeur est la note ''sol''. Comment appelle-t-on en musique classique l'accord appelé « ''do'' sept » (C<sup>7</sup>) en jazz ? Les tonalités dont le ''do'' est la dominante sont les tonalités de ''fa'' majeur (''si''♭ à la clef) et de ''fa'' mineur harmonique (''si''♭, ''mi''♭, ''la''♭ et ''ré''♭ à la clef et ''mi''♮ accidentel). Il s'agit donc de l'accord de septième de dominante des tonalités de ''fa'' majeur et ''fa'' mineur harmonique. {| class="wikitable" |+ Accords fréquents pour quelques la tonalités majeures ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Accord parfait<br />{{Times New Roman|I<sup>5</sup>}} ! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}} |- |''Do'' majeur || || C<br />''do-mi-sol'' || G7<br />''sol-si-ré-fa'' |- |''Sol'' majeur || ''fa''♯ || G<br />''sol-si-ré'' || D7<br />''ré-fa''♯''-la-do'' |- |''Ré'' majeur || ''fa''♯, ''do''♯ || D<br />''ré-fa''♯''-la'' || A7<br />''la-do''♯''-mi-sol'' |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A<br />''la-do''♯''-mi'' || E7<br />''mi-sol''♯''-si-ré'' |- | ''Fa'' majeur || ''si''♭ || F<br />''fa-la-do'' || C7<br />''do-mi-sol-si''♭ |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭<br />''si''♭''-ré-fa'' || F7<br />''fa-la-do-mi''♭ |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭<br />''mi''♭''-sol-si''♭ || B♭7<br />''si''♭''-ré-fa-la''♭ |} {| class="wikitable" |+ Accords fréquents pour quelques la tonalités mineures harmoniques ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Accord parfait<br />{{Times New Roman|i<sup>5</sup>}} ! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}} |- |''La'' mineur<br />harmonique || || Am, A–<br />''la-do-mi'' || E7<br />''mi-sol''♯''-si-ré'' |- |''Mi'' mineur<br />harmonique || ''fa''♯ || Em, E–<br />''mi-sol-si'' || B7<br />''si-ré''♯''-fa''♯''-la'' |- |''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || Bm, B–<br />''si-ré-fa''♯ || F♯7<br />''fa''♯''la''♯''-do''♯''-mi'' |- |''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || F♯m, F♯–<br />''fa''♯''-la-do''♯ || C♯7<br />''do''♯''-mi''♯''-sol''♯''-si'' |- | ''Ré'' mineur<br />harmonique || ''si''♭ || Dm, D–<br />''ré-fa-la'' || A7<br />''la-do''♯''-mi-sol'' |- | ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || Gm, G–<br />''sol-si''♭''-ré'' || D7<br />''ré-fa''♯''-la-do'' |- | ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || Cm, C–<br />''do-mi''♭''-sol'' || G7<br />''sol-si''♮''-ré-fa'' |} {{clear}} == Accords sur les degrés d'une gamme == === Harmonisation d'une gamme === [[Fichier:Accord trois notes gamme do majeur chiffre.svg|vignette|upright=1.2|Accords de trois note sur la gamme de ''do'' majeur, chiffrés.]] On peut ainsi construire une triade par degré d'une gamme. Pour une gamme majeure, les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|V<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|II<sup>5</sup>}}, {{Times New Roman|III<sup>5</sup>}}, {{Times New Roman|VI<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|ii<sup>5</sup>}}, {{Times New Roman|iii<sup>5</sup>}}, {{Times New Roman|vi<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}. Les accords ont tous une quinte juste à l'exception de l'accord {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} qui a une quinte diminuée, raison pour laquelle le « 5 » est barré. C'est un accord dit « de quinte diminuée ». En jazz, l'accord diminué est noté « dim », « ° », « m<sup>♭5</sup> » ou « <sup>–♭5</sup> ». Nous avons donc trois types d'accords (dans la notation jazz) : X (triade majeure), Xm (triade mineure) et X° (triade diminuée), la lettre X remplaçant le nom de la note fondamentale. {{clear}} [[Fichier:Accord trois notes gamme la mineur chiffre.svg|vignette|upright=1.2|Accords de trois notes sur une gamme de ''la'' mineur harmonique, chiffrés.]] Pour une gamme mineure harmonique, les accords {{Times New Roman|III<sup>+5</sup>}}, {{Times New Roman|V<sup>♯</sup>}} et {{Times New Roman|VI<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|II<sup><s>5</s></sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|i<sup>5</sup>}}, {{Times New Roman|ii<sup><s>5</s></sup>}}, {{Times New Roman|iv<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}. Les accords {{Times New Roman|ii<sup><s>5</s></sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} ont une quinte diminuée ; ce sont des accords dits « de quinte diminuée ». L'accord {{Times New Roman|III<sup>+5</sup>}} a une quinte augmentée ; le signe « plus » indique que la note de cinquième, le ''sol'' dièse, est la sensible. En jazz, l'accord est noté « aug » ou « <sup>+</sup> ». Les autres accords ont une quinte juste. Aux trois accords générés par une gamme majeure (X, Xm et X°), nous voyons ici apparaître un quatrième type d'accord : la triade augmentée X<sup>+</sup>. Nous remarquons que des gammes ont des accords communs. Par exemple, l'accord {{Times New Roman|ii<sup>5</sup>}} de ''do'' majeur est identique à l'accord {{Times New Roman|iv<sup>5</sup>}} de ''la'' mineur (il s'agit de l'accord Dm). Quel que soit le mode, les accords construits sur la sensible (accord de quinte diminuée) sont rarement utilisés. S'ils le sont, c'est en tant qu'accord de septième de dominante sans fondamentale (voir ci-après). C'est la raison pour laquelle le chiffrage indique le degré {{Times New Roman|V}} entre guillemets, et non pas le degré {{Times New Roman|VII}} (mais pour des raisons de clarté, nous l'indiquons entre parenthèses au début). En mode mineur, l'accord de quinte augmentée {{Times New Roman|iii<sup>+5</sup>}} est très peu utilisé (voir plus loin ''[[#Progression_d'accords|Progression d'accords]]''). C'est un accord considéré comme dissonant. On voit que : * un accord parfait majeur peut appartenir à cinq gammes différentes ;<br /> par exemple l'accord parfait de ''do'' majeur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> degré de la gamme de ''do'' majeur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' mineur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''mi'' mineur ; * un accord parfait mineur peut appartenir à cinq gammes différentes ;<br />par exemple l'accord parfait de ''la'' mineur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> de la gamme de ''la'' mineur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''mi'' mineur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|III}}<sup>e</sup> degré de ''fa'' majeur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''do'' majeur ; * un accord de quinte diminuée peut appartenir à trois gammes différentes ;<br />par exemple, l'accord de quinte diminuée de ''si'' est l'accord construit sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' majeur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''la'' mineur et sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' mineur ; * un accord de quinte augmentée (à l'état fondamental) ne peut appartenir qu'à une seule gamme ;<br /> par exemple, l'accord de quinte augmentée de ''do'' est l'accord construit sur le {{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur. {| class="wikitable" |+ Notation jazz des triades |- | rowspan="2" colspan="2" | ! scope="col" colspan="2" | Tierce |- ! scope="col" | 3m ! scope="col" | 3M |- ! rowspan="3" | Quinte ! scope="row" | 5d | Xᵒ, X<sub>m</sub><sup>(♭5)</sup> || |- ! scope="row" | 5J | Xm, X– || X |- ! scope="row" | 5A | || X+, X<sup>(♯5)</sup> |} === Harmonisation par des accords de septième === [[Fichier:Harmonisation gamme do majeur par septiemes chiffre.svg|vignette|upright=2|Harmonisation de la gamme de do majeur par des accords de septième.]] Les accords de septième contiennent une dissonance et créent ainsi une tension. Ils sont très utilisés en jazz. Nous avons représenté ci-contre l'harmonisation de la gamme de ''do'' majeur. La constitution des accords est la suivantes : * tierce majeure (3M) ** quinte juste (5J) *** septième mineure (7m) : sur le degré V, c'est l'accord de septième de dominante V<sup>7</sup><sub>+</sub>, noté X<sup>7</sup> (X pour G), *** septième majeure (7M) : sur les degrés I et IV, appelés « accords de septième majeure » et notés aussi X<sup>maj7</sup> ou X<sup>Δ</sup> (X pour C ou F) ; * tierce mineure (3m) ** quinte juste (5J) *** septième mineure : sur les degrés ii, iii et vi, appelés « accords mineur septième » et notés Xm<sup>7</sup> ou X–<sup>7</sup> (X pour D, E ou A), ** quinte diminuée (5d) *** septième mineure (7m) : sur le degré vii, appelé « accord demi-diminué » (puisque seule la quinte est diminuée) et noté X<sup>∅</sup> ou Xm<sup>7(♭5)</sup> ou X–<sup>7(♭5)</sup> (X pour B) ;<br /> en musique classique, on considère que c'est un accord de neuvième de dominante sans fondamentale. Nous avons donc quatre types d'accords : X<sup>7</sup>, X<sup>maj7</sup>, Xm<sup>7</sup> et X<sup>∅</sup> En jazz, on ajoute souvent la quarte à l'accord de sous-dominante IV (sur le ''fa'' dans une gamme de ''do'' majeur) ; il s'agit ici d'une quarte augmentée (''fa''-''si'') et l'accord est surnommé « accord lydien » mais cette dénomination est erronée (il s'agit d'une mauvaise interprétation de textes antiques). C'est un accord de onzième sans neuvième (la onzième étant l'octave de la quarte), il est noté X<sup>maj7(♯11)</sup> ou X<sup>Δ(♯11)</sup> (ici, F<sup>maj7(♯11)</sup>, ''fa''-''la''-''do''-''mi''-''si'' ou ''fa''-''la''-''si''-''do''-''mi''). {| class="wikitable" |+ Chiffrage jazz des accords de septième |- ! scope="col" rowspan="2" | Tierce ! scope="col" rowspan="2" | Quinte ! scope="col" colspan="2" | Septième |- ! scope="col" | 7m ! scope="col" | 7M |- | rowspan="2" | 3m | 5d || X<sup>∅</sup>, X<sub>m</sub><sup>7(♭5)</sup>, X–<sup>7(♭5)</sup> || |- | rowspan="2" | 5J | X<sub>m</sub><sup>7</sup>, X–<sup>7</sup> || |- | rowspan="2" | 3M | X<sup>7</sup> || X<sup>maj7</sup>, X<sup>Δ</sup> |- | 5A || || X<sub>+</sub><sup>maj7</sup>, X<sub>+</sub><sup>Δ</sup> |} === Modulation et emprunt === Un morceau peut comporter des changements de tonalité; appelés « modulation ». Il y a parfois un court passage dans une tonalité différente, typiquement sur une ou deux mesures, avant de retourner dans la tonalité d'origine : on parle d'emprunt. Lorsqu'il y a une modulation ou un emprunt, les degrés changent. Un même accord peut donc avoir une fonction dans une partie du morceau et une autre fonction ailleurs. L'utilisation d'accord différents, et en particulier d'accord utilisant des altérations accidentelles, indique clairement une modulation. Nous avons vu précédemment que les modulations courantes sont : * les modulations dans les tons voisins ; * les modulations homonymes ; * les marches harmoniques. Une modulation entre une tonalité majeure et mineure change la couleur du passage, * la modulation la plus « douce » est entre les tonalités relatives (par exemple''do'' majeur et ''la'' mineur) car ces tonalités utilisent quasiment les mêmes notes ; * la modulation la plus « voyante » est la modulation homonyme (par exemple entre ''do'' majeur et ''do'' mineur). Une modulation commence souvent sur l'accord de dominante de la nouvelle tonalité. Pour analyser un œuvre, ou pour improviser sur une partie, il est important de reconnaître les modulations. La description de la successind es tonalités s'appelle le « parcours tonal ». === Exercices élémentaires === L'apprentissage des accords passe par quelques exercices élémentaires. '''1. Lire un accord''' Il s'agit de lecture de notes : des notes composant les accords sont écrites « empilées » sur une portée, il faut les lire en énonçant les notes de bas en haut. '''2. Reconnaître la « couleur » d'un accord''' On écoute une triade et il faut dire si c'est une triade majeure ou mineure. Puis, on complexifie l'exercice en ajoutant la septième. '''3. Chiffrage un accord''' Trouver le nom d'un accord à partir des notes qui le composent. '''4. Réalisation d'un accord''' Trouver les notes qui composent un accord à partir de son nom. '''5. Dictée d'accords''' On écoute une succession d'accords et il faut soit écrire les notes sur une portée, soit écrire les noms de accords. [[File:Exercice constitution accord basse chiffree.svg|thumb|Exercice : constitution d'accord à partir de la basse chiffrée.]] '''Exercices de basse chiffrée''' ''Réalisation d'un accord'' Sur la figure suivante, écrire les notes des accords correspondant à la basse chiffrée. Déterminer le degré de la fondamentale pour chaque accord en considérant que nous sommes dans la tonalité de ''sol'' majeur. {{boîte déroulante/début|titre=Solution}} [[File:Exercice constitution accord basse chiffree solution.svg|vignette|Solution.]] # La note de basse est un ''do''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''mi'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''sol''.<br />Le chiffrage « <sup>5</sup> » indique que c'est un accord dans son état fondamental (l'écart entre deux notes consécutives ne dépasse pas la tierce), la fondamentale est donc la basse, ''do'', qui est le degré IV de la tonalité. # La note de basse est un ''si''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''ré'', puis nous appliquons le chiffrage 6 et ajoutons la sixte, ''sol''.<br />Le chiffrage « <sup>6</sup> » indique que c'est un accord dans son premier renversement. En le remettant dans son état fondamental, nous obtenons ''sol-si-ré'', la fondamentale est donc la tonique, le degré I. # La note de basse est un ''la''. Nous ajoutons la tierce (chiffre 3), ''do'', et la sixte (6), ''fa''♯. Nous vérifions que le ''fa''♯ est la sensible (signe +)<br />Nous voyons un « blanc » entre les notes ''do'' et ''fa''♯. En descendant le ''fa''♯ à l'octave inférieure, nous obtenons un empilement de tierces ''fa''♯-''la-do'', le fondamentale est donc ''fa''♯, le degré VII. Nous pouvons le voir comme le deuxième renversement de l'accord de septième de dominante, sans fondamentale. # La note de basse est un ''fa''♯. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''la'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''do'' ; nous vérifions qu'il s'agit bien d'une quinte diminuée (le 5 est barré). Nous appliquons le chiffre 6 et ajoutons la sixte, ''ré''.<br />Nous voyons que les notes ''do'' et ''ré'' sont conjointes (intervalle de seconde). En descendant le ''ré'' à l'octave inférieure, nous obtenons un empilement de tierces ''ré-fa''♯-''la-do'', le fondamentale est donc ''ré'', le degré V. Nous constatons que l'accord chiffré est le premier renversement de l'accord de septième de dominante. {{boîte déroulante/fin}} {{clear}} ''Chiffrage d'accords'' [[Fichier:Exercice chiffrage accord basse chiffree.svg|vignette|Accords à chiffrer.]] Chiffrer les accords ci-contre. {{boîte déroulante/début|titre=Solution}} [[File:Exercice chiffrage accord basse chiffree solution.svg|vignette|Solution.]] # On relève les intervalles en partant de la basse : tierce majeure (3M) et quinte juste (5J). Le chiffrage complet est donc ''fa''<sup>5</sup><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''fa''<sup>5</sup>.<br /> On peut aussi reconnaître que c'est l'accord parfait sur la tonique de la tonalité de ''fa'' majeur dans son état fondamental, le chiffrage d'un accord parfait étant <sup>5</sup>. # On relève les intervalles en partant de la basse : quarte juste (4J), sixte majeure (6M). Le chiffrage complet est donc ''fa''<sup>6</sup><sub>4</sub>.<br /> On peut aussi reconnaître que c'est le second renversement de l'accord ''mi-sol-si'', sur la tonique de la tonalité de ''mi'' mineur, le chiffrage du second renversement d'un accord parfait étant <sup>6</sup><sub>4</sub>. # Les intervalles en partant de la basse sont : tierce mineure (3m), quinte diminuée (5d), sixte mineure (6m). Le chiffrage complet est donc ''mi''<sup>6</sup><small><s>5</s></small><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''mi''<sup>6</sup><sub><s>5</s></sub>.<br /> On reconnaît le premier renversement de l'accord ''do-mi-sol-si''♭, accord de septième de dominante de la tonalité de ''fa'' majeur. # Les intervalles en partant de la basse sont : tierce mineure (3m), quinte juste (5J), septième mineure (7m). Le chiffrage complet est donc ''ré''<sup>7</sup><small>5</small><sub>3</sub> ; c'est typique d'un accord de septième de dominante, son chiffrage est donc ''ré''<sup>7</sup><sub>+</sub>.<br /> On reconnaît l'accord de septième de dominante de la tonalité de ''sol'' mineur dans son état fondamental. {{boîte déroulante/fin}} {{clear}} [[File:Exercice constitution accord notation jazz.svg|thumb|Exercice : constitution d'un accord d'après son chiffrage en notation jazz.]] '''Exercices de notation jazz''' ''Réalisation d'un accord'' Sur la figure suivante, écrire les notes des accords correspondant aux chiffrages. {{boîte déroulante/début|titre=Solution}} [[File:Exercice constitution accord notation jazz solution.svg|thumb|Solution.]] # Il s'agit de la triade majeure de ''do'' dans son état fondamental. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''do-mi-sol''. # Il s'agit de la triade majeure de ''sol''. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''sol-si-ré''. On renverse l'accord afin que la basse soit le ''si'', l'accord est donc ''si-ré-sol''. # Il s'agit de l'accord demi-diminué de ''fa''♯. Les intervalles sont la tierce mineure (3m), la quinte diminuée (5d) et la septième mineure (7m). Les notes sont donc ''fa''♯-''la-do-mi''. Nous renversons l'accord afin que la basse soit le ''la'', l'accord est donc ''a-do-mi-fa''♯. # Il s'agit de l'accord de septième de ''ré''. Les intervalles sont donc la tierce majeure (3M), la quinte juste (5J) et la septième mineure (7m). Les notes sont ''ré-fa''♯''-la-do''. Nous renversons l'accord afin que la basse soit le ''fa''♯, l'accord est donc ''fa''♯''-la-do-ré''. {{boîte déroulante/fin}} {{clear}} ''Chiffrage d'accords'' [[File:Exercice chiffrage accord notation jazz.svg|thumb|Accords à chiffrer.]] Chiffrer les accords ci-contre. {{boîte déroulante/début|titre=Solution}} [[File:Exercice chiffrage accord notation jazz solution.svg|thumb|Solution.]] # Les notes sont toutes sur des interlignes consécutifs, c'est donc un empilement de tierces ; l'accord est dans son état fondamental. Les intervalles sont une tierce majeure (''fa-la'' : 3M) et une quinte juste (''fa-do'' : 5J), c'est donc la triade majeure de ''fa''. Le chiffrage est F. # Il y a un blanc dans l'empilement des notes, c'est donc un accord renversé. En permutant les notes pour n'avoir que des tierces, on trouve l'accord ''mi-sol-si''. Les intervalles sont une tierce mineure (''mi-sol'' : 3m) et une quinte juste (''mi-si'' : 5J), c'est donc la triade mineure de ''mi'' avec un ''si'' à la basse. Le chiffrage est Em/B ou E–/B. # Il y deux notes conjointes, c'est donc un renversement. L'état fondamental de cet accord est ''do-mi-sol-si''♭. Les intervalles sont une tierce majeure (''do-mi'' : 3M), une quinte juste (''do-sol'' : 5J) et une septième mineure (''do-si''♭ : 7m). C'est donc l'accord de ''do'' septième avec un ''mi'' à la basse, chiffré C<sup>7</sup>/E. # Les notes sont toutes sur des interlignes consécutifs, l'accord est dans son état fondamental. Les intervalles sont la tierce mineure (''ré-fa'' : 3m), une quinte juste (''ré-la'' : 5J) et une septième mineure (''ré-do'' : 7m). C'est donc l'accord de ''ré'' mineur septième, chiffré Dm<sup>7</sup> ou D–<sup>7</sup>. {{boîte déroulante/fin}} {{clear}} == Harmonie fonctionnelle == Le choix des accords et de leur succession — la progression des accords — est un élément important d'un morceau, de sa composition. Le compositeur ou la compositrice a bien sûr une liberté totale, mais pour faire des choix, il faut comprendre les conséquences de ces choix, et donc ici, les effets produits par les accords et leur progression. Une des manières d'aborder le sujet est l'harmonie fonctionnelle. === Les trois fonctions des accords === En harmonie tonale, on considère que les accords ont une fonction. Il existe trois fonctions : * la fonction de tonique, {{Times New Roman|I}} ; * la fonction de sous-dominante, {{Times New Roman|IV}} ; * la fonction de dominante, {{Times New Roman|V}}. L'accord de tonique, {{Times New Roman|I}}, est l'accord « stable » de la tonalité par excellence. Il conclut en général les morceaux, et ouvre souvent les morceaux ; il revient fréquemment au cours du morceau. L'accord de dominante, {{Times New Roman|V}}, est un accord qui introduit une instabilité, une tension. En particulier, il contient la sensible (degré {{Times New Roman|VI}}), qui est une note « aspirée » vers la tonique. Cette tension, qui peut être renforcée par l'utilisation d'un accord de septième, est fréquemment résolue par un passage vers l'accord de tonique. Nous avons donc deux mouvements typiques : {{Times New Roman|I}} → {{Times New Roman|V}} (création d'une tension, d'une attente) et {{Times New Roman|V}} → {{Times New Roman|I}} (résolution d'une tension). Les accords de tonique et de dominante ont le cinquième degré en commun, cette note sert donc de pivot entre les deux accords. L'accord de sous-dominante, {{Times New Roman|IV}}, est un accord qui introduit lui aussi une tension, mais moins grande : il ne contient pas la sensible. Notons que s'il est une quarte au-dessus de la tonique, il est aussi une quinte en dessous d'elle ; il est symétrique de l'accord de dominante. Il a donc un rôle similaire à l'accord de dominante, mais atténué. L'accord de sous-dominante aspire soit vers l'accord de dominante, très proche, et l'on a alors une augmentation de la tension ; soit vers l'accord de tonique, un retour vers la stabilité (il a alors un rôle semblable à la dominante). Du fait de ces deux bifurcations possibles — augmentation de la tension ({{Times New Roman|IV}} → {{Times New Roman|V}}) ou retour à la stabilité ({{Times New Roman|IV}} → {{Times New Roman|I}}) —, l'utilisation de l'accord de sous-dominante introduit un certain flottement : si l'on peut facilement prédire l'accord qui suit un accord de dominante, on ne peut pas prédire ce qui suit un accord de sous-dominante. Notons que la composition ne consiste pas à suivre ces règles de manière stricte, ce qui conduirait à des morceaux stéréotypés et plats. Le plaisir d'écoute joue sur une alternance entre satisfaction d'une attente (respect des règles) et surprise (rompre les règles). === Accords remplissant ces fonctions === Les accords sur les autres degrés peuvent se ramener à une de ces trois fonctions : * {{Times New Roman|II}} : fonction de sous-dominante {{Times New Roman|IV}} ; * {{Times New Roman|III}} (très peu utilisé en mode mineur en raison de sa dissonance) et {{Times New Roman|VI}} : fonction de tonique {{Times New Roman|I}} ; * {{Times New Roman|VII}} : fonction de dominante {{Times New Roman|V}}. En effet, les accords étant des empilements de tierces, des accords situés à une tierce l'un de l'autre — {{Times New Roman|I}} ↔ {{Times New Roman|III}}, {{Times New Roman|II}} ↔ {{Times New Roman|IV}}, {{Times New Roman|V}} ↔ {{Times New Roman|VII}}, {{Times New Roman|VI}} ↔ {{Times New Roman|VIII}} ( = {{Times New Roman|I}}) — ont deux notes en commun. On retrouve le fait que l'accord sur le degré {{Times New Roman|VII}} est considéré comme un accord de dominante sans tonique. En mode mineur, l'accord sur le degré {{Times New Roman|III}} est évité, il n'a donc pas de fonction. {|class="wikitable" |+ Fonction des accords |- ! scope="col" | Fondamentale ! scope="col" | Fonction |- | {{Times New Roman|I}} || tonique |- | {{Times New Roman|II}} || sous-dominante faible |- | {{Times New Roman|III}} || tonique faible |- | {{Times New Roman|IV}} || sous-dominante |- | {{Times New Roman|V}} || dominante |- | {{Times New Roman|VI}} || tonique faible |- | {{Times New Roman|VII}} || dominante faible |} Par exemple en ''do'' majeur : * fonction de tonique : '''''do''<sup>5</sup> (C)''', ''mi''<sup>5</sup> (E–), ''la''<sup>5</sup> (A–) ; * fonction de sous-dominante : '''''fa''<sup>5</sup> (F)''', ''ré''<sup>5</sup> (D–) ; * fonction de dominante : '''''sol''<sup>5</sup> (G)''' ou ''sol''<sup>7</sup><sub>+</sub> (G<sup>7</sup>), ''si''<sup> <s>5</s></sup> (B<sup>o</sup>). En ''la'' mineur harmonique : * fonction de tonique : '''''la''<sup>5</sup> (A–)''', ''fa''<sup>5</sup> (F) [, rarement : ''do''<sup>+5</sup> (C<sup>+</sup>)] ; * fonction de sous-dominante : '''''ré''<sup>5</sup> (D–)''', ''si''<sup> <s>5</s></sup> (B<sup>o</sup>) ; * fonction de dominante : '''''mi''<sup>5</sup> (E)''' ou ''mi''<sup>7</sup><sub>+</sub> (E<sup>7</sup>), ''sol''♯<sup> <s>5</s></sup> (G♯<sup>o</sup>). Le fait d'utiliser des accords différents pour remplir une fonction permet d'enrichir l'harmonie, et de jouer sur l'équilibre entre satisfaction d'une attente (on respecte les règles sur les fonctions) et surprise (mais on n'utilise pas l'accord attendu). === Les dominantes secondaires === On utilise aussi des accords de septième dominante se fondant sur un autre degré que la dominante de la gamme ; on parle de « dominante secondaire ». Typiquement, avant un accord de septième de dominante, on utilise parfois un accord de dominante de dominante, dont le degré est alors noté « {{Times New Roman|V}} de {{Times New Roman|V}} » ou « {{Times New Roman|V}}/{{Times New Roman|V}} » ; la fondamentale est de l'accord est alors situé cinq degrés au-dessus de la dominante ({{Times New Roman|V}}), c'est donc le degré {{Times New Roman|IX}}, c'est-à-dire le degré {{Times New Roman|II}} de la tonalité en cours). Ou encore, on utilise un accord de dominante du degré {{Times New Roman|IV}} (« {{Times New Roman|V}} de {{Times New Roman|IV}} », la fondamentale est alors le degré {{Times New Roman|I}}) avant un accord sur le degré {{Times New Roman|IV}} lui-même. Par exemple, en tonalité de ''do'' majeur, on peut trouver un accord ''ré - fa''♯'' - la - do'' (chiffré {{Times New Roman|V}} de {{Times New Roman|V}}<sup>7</sup><sub>+</sub>), avant un accord ''sol - si - ré - fa'' ({{Times New Roman|V}}<sup>7</sup><sub>+</sub>). L'accord ''ré - fa''♯'' - la - do'' est l'accord de septième de dominante des tonalités de ''sol''. Dans la même tonalité, on pourra utiliser un accord ''do - mi - sol - si''♭ ({{Times New Roman|V}} de {{Times New Roman|IV}}<sup>7</sup><sub>+</sub>) avant un accord ''fa - la - do'' ({{Times New Roman|IV}}<sup>5</sup>). Le recours à une dominante secondaire peut atténuer une transition, par exemple avec un enchaînement ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> → ''fa''<sup>5</sup> (C → C<sup>7</sup> → F) qui correspond à un enchaînement {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}} : le passage ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> (C → C<sup>7</sup>) se fait en ajoutant une note (le ''si''♭) et rend naturel le passage ''do'' → ''fa''. Sur les sept degré de la gamme, on ne considère en général que cinq dominantes secondaires : en effet, la dominante du degré {{Times New Roman|I}} est la dominante « naturelle, primaire » de la tonalité (et n'est donc pas secondaire) ; et utiliser la dominante de {{Times New Roman|VII}} consisterait à considérer l'accord de {{Times New Roman|VII}} comme un accord propre, on évite donc les « {{Times New Roman|V}} de “{{Times New Roman|V}}” » (mais les « “{{Times New Roman|V}}” de {{Times New Roman|V}} » sont tout à fait « acceptables »). === Enchaînements classiques === Nous avons donc vu que l'on trouve fréquemment les enchaînements suivants : * pour créer une instabilité : ** {{Times New Roman|I}} → {{Times New Roman|V}}, ** {{Times New Roman|I}} → {{Times New Roman|IV}} (instabilité moins forte mais incertitude sur le sens d'évolution) ; * pour maintenir l'instabilité : ** {{Times New Roman|IV}} → {{Times New Roman|V}} ; * pour résoudre l'instabilité : ** {{Times New Roman|IV}} → {{Times New Roman|I}}, ** {{Times New Roman|V}} → {{Times New Roman|I}}, cas particuliers (voir plus bas) : *** {{Times New Roman|V}}<sup>+4</sup> → {{Times New Roman|I}}<sup>6</sup>, *** {{Times New Roman|I}}<sup>6</sup><sub>4</sub> → {{Times New Roman|V}}<sup>7</sup><sub>+</sub> → {{Times New Roman|I}}<sup>5</sup>. Les degrés indiqués ci-dessus sont les fonctions ; on peut donc utiliser les substitutions suivantes : * {{Times New Roman|I}} par {{Times New Roman|VI}} et, en tonalité majeure, {{Times New Roman|III}} ; * {{Times New Roman|IV}} par {{Times New Roman|II}} ; * {{Times New Roman|V}} par {{Times New Roman|VII}}. Pour enrichir l'harmonie, on peut utiliser les dominantes secondaires, en particulier : * {{Times New Roman|V}} de {{Times New Roman|V}} ({{Times New Roman|II}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|V}}, * {{Times New Roman|V}} de {{Times New Roman|IV}} ({{Times New Roman|I}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|IV}}. On peut enchaîner les enchaînements, par exemple {{Times New Roman|I}} → {{Times New Roman|IV}} → {{Times New Roman|V}}, ou encore {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}}… En jazz, on utilise très fréquemment l'enchaînement {{Times New Roman|II}} → {{Times New Roman|V}} → {{Times New Roman|I}} (deux-cinq-un). On peut bien sûr avoir d'autres enchaînements, mais ces règles permettent d'analyser un grand nombre de morceaux, et donnent des clefs utiles pour la composition. Nous voyons ci-après un certain nombre d'enchaînements courants dans différents styles == Exercice == Un hautboïste travaille la sonate en ''do'' mineur S. 277 de Heinichen. Sur le deuxième mouvement ''Allegro'', il a du mal à travailler un passage en raison des altérations accidentelles. Sur la suggestion de sa professeure, il décide d'analyser la progression d'accords sous-jacente afin que les altérations deviennent logiques. Il s'agit d'un duo hautbois et basson pour lequel les accords ne sont pas chiffrés, le basson étant ici un instrument soliste et non pas un élément de la basse continue. Sur l'extrait suivant, déterminez les basses et la qualité (chiffrage) des accords sous-jacents. Commentez. [[Fichier:Sonate hautbois basson heinichen 2e mvt mes49.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen.]] {{note|L'œuvre est en ''do'' mineur et devrait donc avoir trois bémols à la clef, or ici il n'y en a que deux. En effet, le ''la'' pouvant être bécarre en mode mineur mélodique ascendant, le compositeur a préféré le noter explicitement en altération accidentelle lorsque l'on est en mode mélodique naturel, harmonique ou mélodique descendant. C'est un procédé assez courant à l'époque baroque.}} {{boîte déroulante/début|titre=Solution}} Une des difficultés ici est que les arpèges joués par les instruments sont agrémentés de notes de passage. Les notes de la basse (du basson) sont différentes entre le premier et le deuxième temps de chaque mesure et ne peuvent pas appartenir au même accord. On a donc un accord par temps. Sur le premier temps de chaque mesure, le basson joue une octave. La note concernée est donc la basse de chaque accord. Pour savoir s'il s'agit d'un accord à l'état fondamental ou d'un renversement, on regarde ce que joue le hautbois : dans un mouvement conjoint (succession d'intervalles de secondes), il est difficile de distinguer les notes de l'arpège des notes de passage, mais : les notes des grands intervalles font partie de l'accord. Ainsi, sur le premier temps de la première mesure (la basse est un ''mi''♭), on a une sixte descendante ''sol''-''si''♭ et, à la fin du temps, une tierce descendante ''sol''-''mi''♭. L'accord est donc ''mi''♭-''sol''-''si''♭, c'est un accord de quinte (accord parfait à l'état fondamental). À la fin du premier temps, le basson joue un ''do'', c'est donc une note étrangère. Sur le second temps de la première mesure, le basson joue une tierce ascendante ''fa''-''la''♭, la première note est la basse de l'accord et la seconde une des notes de l'accord. Le hautbois commence par une sixte descendante ''la''♭-''do'', l'accord est donc ''fa''-''la''♭-''do'', un accord de quinte (accord parfait à l'état fondamental). Le ''do'' du basson la fin du premier temps est donc une anticipation. Les autres notes étrangères de la première mesure sont des notes de passage. Mais il faut faire attention : en suivant ce principe, sur les premiers temps des deuxième et troisième mesure, nous aurions des accords de septième d'espèce (puisque la septième est majeure). Or, on ne trouve pas, ou alors exceptionnellement, d'accord de septième d'espèce dans le baroque, mais quasi exclusivement des accords de septième de dominante. Donc au début de la deuxième mesure, le ''la''♮ est une appoggiature du ''si''♭, l'accord est donc ''si''♭-''ré''-''fa'', un asscord de quinte. De même, au début de la troisième mesure, le ''sol'' est une appoggiature du ''la''♭. Il faut donc se méfier d'une analyse purement « mathématique ». Il faut s'attacher à ressentir la musique, et à connaître les styles, pour faire une analyse pertinente. Ci-dessous, nous avons grisé les notes étrangères. [[Fichier:Sonate hautbois basson heinichen 2e mvt mes49 analyse.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen. Analyse de la progression harmonique.]] Le chiffrage jazz équivalent est : : | E♭ F– | B♭<sup>Δ</sup> E♭ | A♭<sup>Δ</sup> D– | G … Nous remarquons une progression assez régulière : : ''mi''♭ ↗[2<sup>de</sup>] ''fa'' | ↘[5<sup>te</sup>] ''si''♭ ↗[4<sup>te</sup>] ''mi''♭ | ↘[5<sup>te</sup>] ''la''♭ ↗[4<sup>te</sup>] ''ré'' | ↘[5<sup>te</sup>] ''sol'' Le ''mi''♭ est le degré {{Times New Roman|III}} de la tonalité principale (''do'' mineur), c'est donc une tonique faible ; il « joue le même rôle » qu'un ''do''. S'il y avait eu un accord de ''do'' au début de l'extrait, on aurait eu une progression parfaitement régulière ↗[4<sup>te</sup>] ↘[5<sup>te</sup>]. Nous avons les modulations suivantes : * mesure 49 : ''do'' mineur naturel (le ''si''♭ n'est pas une sensible) avec un accord sur “{{Times New Roman|I}}” (tonique faible, {{Times New Roman|III}}, pour la première analyse, ou bien tonique forte, {{Times New Roman|I}}, pour la seconde) suivi d'un accord sur {{Times New Roman|IV}} ; * mesure 50 : ''si''♭ majeur avec un accord sur {{Times New Roman|I}} suivi d'un accord sur {{Times New Roman|IV}} ; * mesure 51 : ''la''♭ majeur avec un accord sur {{Times New Roman|I}}, et emprunt à ''do'' majeur avec un accord sur {{Times New Roman|II}} ({{Times New Roman|IV}} faible). On a donc une marche harmonique {{Times New Roman|I}} → {{Times New Roman|IV}} qui descend d'une seconde majeure (un ton) à chaque mesure (''do'' → ''si''♭ → ''la''♭), avec une exception sur la dernière mesure (modulation en cours de mesure et descente d'une seconde mineure au lieu de majeure). Ce passage est donc construit sur une régularité, une règle qui crée un effet d'attente — enchaînement {{Times New Roman|I}}<sup>5</sup> → {{Times New Roman|IV}}<sup>5</sup> avec une marche harmonique d'une seconde majeure descendante —, et des « surprises », des exceptions au début — ce n'est pas un accord {{Times New Roman|I}}<sup>5</sup> mais un accord {{Times New Roman|III}}<sup>5</sup> — et à la fin — modulation en milieu de mesure et dernière descente d'une seconde mineure (½t ''la''♭ → ''sol''). L'extrait ne permet pas de le deviner, mais la mesure 52 est un retour en ''do'' mineur, avec donc une modulation sur la dominante (accord de ''sol''<sup>7</sup><sub>+</sub>, G<sup>7</sup>). {{boîte déroulante/fin}} == Progression d'accords == Comme pour la mélodie, la succession des accords dans un morceau, la progression d'accords, suit des règles. Et comme pour la mélodie, les règles diffèrent d'un style musical à l'autre et la créativité consiste à parfois ne pas suivre ces règles. Et comme pour la mélodie, on part d'un ensemble de notes organisé, d'une gamme caractéristique d'une tonalité, d'un mode. Les accords les plus utilisés pour une tonalité donnée sont les accords dont la fondamentale sont les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la tonalité, en particulier la triade {{Times New Roman|I}}, appelée « accord parfait » ou « accord de tonique », et l'accord de septième {{Times New Roman|V}}, appelé « septième de dominante ». Le fait d'avoir une progression d'accords qui se répète permet de structurer un morceau. Pour les morceaux courts, il participe au plaisir de l'écoute et facilite la mémorisation (par exemple le découpage couplet-refrain d'une chanson). Sur les morceaux longs, une trop grande régularité peut introduire de la lassitude, les longs morceaux sont souvent découpés en parties présentant chacune une progression régulière. Le fait d'avoir une progression régulière permet la pratique de l'improvisation : cadence en musique classique, solo en jazz et blues. ; Note : Le terme « cadence » désigne plusieurs choses différentes, et notamment en harmonie : :* une partie improvisée dans un opéra ou un concerto, sens utilisé ci-dessus ; :* une progression d'accords pour ponctuer un morceau et en particulier pour le conclure, sens utilisé dans la section suivante. === Accords peu utilisés === En mode mineur, l'accord de quinte augmentée {{Times New Roman|III<sup>+5</sup>}} est très peu utilisé. C'est un accord dissonant ; il intervient en général comme appogiature de l'accord de tonique (par exemple en ''la'' mineur : {{Times New Roman|III<sup>+5</sup>}} ''do'' - ''mi'' - ''sol''♯ → {{Times New Roman|I<sup>6</sup>}} ''do'' - ''mi'' - ''la''), ou de l'accord de dominante ({{Times New Roman|III<sup>6</sup><sub>+3</sub>}} ''mi'' - ''sol''♯ - ''do'' → {{Times New Roman|V<sup>5</sup>}} ''mi'' - ''sol''♯ - ''si''). Il peut être aussi utilisé comme préparation à l'accord de sous-dominante (enchaînement {{Times New Roman|III}} → {{Times New Roman|IV}}). Par ailleurs, il a une constitution symétrique — c'est l'empilement de deux tierces majeures — et ses renversements ont les mêmes intervalles à l'enharmonie près (quinte augmentée/sixte mineure, tierce majeure/quarte diminuée). De ce fait, un même accord est commun, par renversement et à l'enharmonie près, à trois tonalités : le premier renversement de l'accord ''do'' - ''mi'' - ''sol''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur) est enharmonique à ''mi'' - ''sol''♯ - ''si''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''do''♯ mineur) ; le second renversement est enharmonique à ''la''♭ - ''do'' - ''mi'' ({{Times New Roman|III}}<sup>e</sup> degré de ''fa'' mineur). === Accords très utilisés === Les trois accords les plus utilisés sont les accords de tonique (degré {{Times New Roman|I}}), de sous-dominante ({{Times New Roman|IV}}) et de dominante ({{Times New Roman|V}}). Ils interviennent en particulier en fin de phrase, dans les cadences. L'accord de dominante sert souvent à introduire une modulation : la modulation commence sur l'accord de dominante de la nouvelle tonalité. On note que l'accord de sous-dominante est situé une quinte juste en dessous de la tonique, les accords de dominante et de sous-dominante sont donc symétriques. En jazz, on utilise également très fréquemment l'accord de la sus-tonique (degré {{Times New Roman|II}}), souvent dans des progressions {{Times New Roman|II}} - {{Times New Roman|V}} (- {{Times New Roman|I}}). Rappelons que l'accord de sus-tonique a la fonction de sous-dominante. === Cadences et ''turnaround'' === Le terme « cadence » provient de l'italien ''cadenza'' et désigne la « chute », la fin d'un morceau ou d'une phrase musicale. On distingue deux types de cadences : * les cadences conclusive, qui créent une sensation de complétude ; * les cadences suspensives, qui crèent une sensation d'attente. ==== Cadence parfaite ==== [[Fichier:Au clair de le lune cadence parfaite.midi|thumb|''Au clair de la lune'', harmonisé avec une cadence parfaite (italienne).]] [[Fichier:Au clair de le lune mineur cadence parfaite.midi|thumb|''Idem'' mais en mode mineur harmonique.]] La cadence parfaite est l'enchaînement de l'accord de dominante suivi de l'accord parfait : {{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}}, les deux accord étant à l'état fondamental. Elle donne une impression de stabilité et est donc très souvent utilisée pour conclure un morceau. C'est une cadence conclusive. On peut aussi utiliser l'accord de septième de dominante, la dissonance introduisant une tension résolue par l'accord parfait : {{Times New Roman|V<sup>7</sup><sub>+</sub> - I<sup>5</sup>}}. Elle est souvent précédée de l'accord construit sur le IV<sup>e</sup> degré, appelé « accord de préparation », pour former la cadence italienne : {{Times New Roman|IV<sup>5</sup> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}}. Elle est également souvent précédée du second renversement de l'accord de tonique, qui est alors appelé « appoggiature de la cadence » : {{Times New Roman|I<sup>6</sup><sub>4</sub> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}} (on remarque que les accords {{Times New Roman|I}}<sup>6</sup><sub>4</sub> et {{Times New Roman|V}}<sup>5</sup> ont la basse en commun, et que l'on peut passer de l'un à l'autre par un mouvement conjoint sur les autres notes). {{clear}} ==== Demi-cadence ==== [[Fichier:Au clair de le lune demi cadence.midi|thumb|''Au clair de la lune'', harmonisé avec une demi-cadence.]] Une demi-cadence est une phrase ou un morceau se concluant sur l'accord construit sur le cinquième degré. Il provoque une sensation d'attente, de suspens. Il s'agit en général d'une succession {{Times New Roman|II - V}} ou {{Times New Roman|IV - V}}. C'est une cadence suspensive. On utilise rarement un accord de septième de dominante. {{clear}} ==== Cadence rompue ou évitée ==== La cadence rompue, ou cadence évitée, est succession d'un accord de dominante et d'un accord de sus-dominante, {{Times New Roman|V}} - {{Times New Roman|VI}}. C'est une cadence suspensive. ==== Cadence imparfaite ==== Une cadence imparfaite est une cadence {{Times New Roman|V - I}}, comme la cadence parfaite, mais dont au moins un des deux accords est dans un état renversé. ==== Cadence plagale ==== La cadence plagale — du grec ''plagios'', oblique, en biais — est la succession de l'accord construit sur le quatrième degré, suivi de l'accord parfait : {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}. Elle peut être utilisée après une cadence parfaite ({{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}} - {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}). Elle donne un caractère solennel, voire religieux — elle est parfois appelée « cadence amen » —, elle a un côté antique qui rappelle la musique modale et médiévale<ref>{{lien web |url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/la-cadence-amen-ou-comment-se-dire-adieu-7191921 |titre=La cadence « Amen » ou comment se dire adieu |auteur=Max Dozolme (MAXXI Classique) |site=France Musique |date=2025-04-25 |consulté le=2025-04-25}}.</ref>. C'est une cadence conclusive. ==== {{lang|en|Turnaround}} ==== [[Fichier:Au clair de le lune turnaround.midi|thumb|Au clair de la lune, harmonisé en style jazz : accords de 7{{e}}, anatole suivie d'un ''{{lang|en|turnaround}}'' ii-V-I.]] Le terme ''{{lang|en|turnaround}}'' signifie revirement, retournement. C'est une succession d'accords que fait la transition entre deux parties, en créant une tension-résolution. Le ''{{lang|en|turnaround}}'' le plus courant est la succession {{Times New Roman|II - V - I}}. On utilise également fréquemment l'anatole : {{Times New Roman|I - VI - II - V}}. {| class="wikitable" |+ Progressions typiques d'accords dans une tonalité majeure ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Cadence<br />parfaite<br /> {{Times New Roman|V - I}} ! scope="col" | Cadence<br />italienne<br />{{Times New Roman|IV - V - I}} ! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou IV - V}} ! scope="col" | Cadence<br />plagale<br />{{Times New Roman|IV - I}} ! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}} ! scope="col" | Anatole<br />{{Times New Roman|I - vi - ii - V}} |- |''Do'' majeur || || G - C || F - G - C || Dm - G ou F - G || F - C || Dm - G - C || C - Am - Dm - G |- |''Sol'' majeur || ''fa''♯ || D - G || C - D - G || Am - D ou C - D || C - G || Am - D - G || G - Em - Am - D |- |''Ré'' majeur || ''fa''♯, ''do''♯ || A - D || G - A - D || Em - A ou G - A || G - D || Em - A - D || D - Bm - Em - A |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || E - A || D - E - A || Bm - E ou D - E || D - A || Bm - E - A || A - F♯m - B - E |- | ''Fa'' majeur || ''si''♭ || C - F || B♭ - C - F || Gm - C ou B♭ - C || B♭ - F || Gm - C - F || F - Dm - Gm - C |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || F - B♭ || E♭ - F - B♭ || Cm - F ou E♭ - F || E♭ - B♭ || Cm - F - B♭ || B♭ - Gm - Cm - F |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || B♭ - E♭ || A♭ - B♭ - E♭ || Fm - B♭ ou A♭ - B♭ || A♭ - E♭ || Fm - B♭ - E♭ || Gm - Cm - Fm - B♭ |} {| class="wikitable" |+ Progressions typiques d'accords dans une tonalité mineure ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Cadence<br />parfaite<br />{{Times New Roman|V - i}} ! scope="col" | Cadence<br />italienne<br />{{Times New Roman|iv - V - i}} ! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou iv - V}} ! scope="col" | Cadence<br />plagale<br />{{Times New Roman|iv - i}} ! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}} ! scope="col" | Anatole<br />{{Times New Roman|i - VI - ii - V}} |- | ''La'' mineur<br />harmonique || || E - Am || Dm - E - Am || B° - E ou Dm - E || Dm - Am || B° - E - Am || Am - F - B° - E |- | ''Mi'' mineur<br />harmonique || ''fa''♯ || B - Em || Am - B - Em || F♯° - B ou Am - B || Am - Em || F♯° - B - Em || Em - C - F♯° - B |- | ''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || F♯ - Bm || Em - F♯ - Bm || C♯° - F♯ ou Em - F♯ || Em - Bm || C♯° - F♯ - Bm || Bm - G - C♯° - F♯ |- | ''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || C♯ - F♯m || Bm - C♯ - F♯m || G♯° - C♯ ou Bm - C♯ || Bm - F♯m || G♯° - C♯ - F♯m || A+ - D - G♯° - C♯ |- | ''Ré'' mineur<br />harmonique || ''si''♭ || A - Dm || Gm - A - Dm || E° - A ou Gm - A || Gm - Dm || E° - A - Dm || Dm - B♭ - E° - A |- | ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || D - Gm || Cm - D - Gm || A° - D ou Cm - D || Cm - Gm|| A° - D - Gm || Gm - E♭ - A° - D |- | ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || G - Cm || Fm - G - Cm || D° - G ou Fm - G || Fm - Dm || D° - G - Cm || Cm - A♭ - D° - G |} ==== Exemple : ''La Mer'' ==== : {{lien web | url = https://www.youtube.com/watch?v=PXQh9jTwwoA | titre = Charles Trenet - La mer (Officiel) [Live Version] | site = YouTube | auteur = Charles Trenet | consulté le = 2020-12-24 }} Le début de ''La Mer'' (Charles Trenet, 1946) est en ''do'' majeur et est harmonisé par l'anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (C - Am - Dm - G<sup>7</sup>) sur deux mesures, jouée deux fois ({{Times New Roman|1=<nowiki>|I-vi|ii-V</nowiki><sup>7</sup><nowiki>|</nowiki>}} × 2). Viennent des variations avec les progressions {{Times New Roman|I-III-vi-V<sup>7</sup>}} (C - E - Am - G<sup>7</sup>) puis la « progression ’50s » (voir plus bas) {{Times New Roman|I-vi-IV-VI<sup>7</sup>}} (C - Am - F - A<sup>7</sup>, on remarque que {{Times New Roman|IV}}/F est le relatif majeur du {{Times New Roman|ii}}/Dm de l'anatole), jouées chacune une fois sur deux mesure ; puis cette première partie se conclut par une demie cadence {{Times New Roman|ii-V<sup>7</sup>}} sur une mesure puis une dernière anatole sur trois mesures ({{Times New Roman|1=<nowiki>|I-vi|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}}). Cela constitue une première partie « A » sur douze mesures qui se termine par une demi-cadence ({{Times New Roman|ii-V<sup>7</sup>}}) qui appelle une suite. Cette partie A est jouée une deuxième fois mais la fin est modifiée pour la transition : les deux dernières mesures {{Times New Roman|<nowiki>|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}} deviennent {{Times New Roman|<nowiki>|ii-V</nowiki><sup>7</sup><nowiki>|I|</nowiki>}} (|Dm-G7|C|), cette partie « A’ » se conclut donc par une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}). Le morceau passe ensuite en tonalité de ''mi'' majeur, donc une tierce au dessus de ''do'' majeur, sur six mesures. Cette partie utilise une progression ’50s {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (E - C♯m - A - B<sup>7</sup>), qui est rappelons-le une variation de l'anatole, l'accord {{Times New Roman|ii}} (Fm) étant remplacé par son relatif majeur {{Times New Roman|IV}} (A). Cette anatole modifiée est jouée deux fois puis la partie en ''mi'' majeur se conclut par l'accord parfait {{Times New Roman|I}} joué sur deux mesures (|E|E|), on a donc, avec la mesure précédente, avec une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}). Suivent ensuite six mesures en ''sol'' majeur, donc à nouveau une tierce au dessus de ''mi'' majeur. Elle comporte une progression {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (G - Em - C - D<sup>7</sup>), donc anatole avec substitution du {{Times New Roman|ii}}/Am par son relatif majeur {{Times New Roman|VI}}/C (progression ’50s), puis une anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (G - Em - Am - D<sup>7</sup>) et deux mesure sur la tonique {{Times New Roman|I-I<sup>7</sup>}} (G - G<sup>7</sup>), formant à nouveau une cadence parfaite. La fin sur un accord de septième, dissonant, appelle une suite. Cette partie « B » de douze mesures comporte donc deux parties similaires « B1 » et « B2 » qui forment une marche harmonique (montée d'une tierce). Le morceau se conclut par une reprise de la partie « A’ » et se termine donc par une cadence parfaite. Nous avons une structure A-A’-B-A’ sur 48 mesures, proche la forme AABA étudiée plus loin. Donc ''La Mer'' est un morceau structuré autour de l'anatole avec des variations (progression ’50s, substitution du {{Times New Roman|ii}} par son relatif majeur {{Times New Roman|IV}}) et comportant une marche harmonique dans sa troisième partie. Les parties se concluent par des ''{{lang|en|turnarounds}}'' sous la forme d'une cadence parfaite ou, pour la partie A, par une demi-cadence. {| border="1" rules="rows" frame="hsides" |+ Structure de ''La Mer'' |- align="center" | | colspan="12" | ''do'' majeur | |- align="center" ! scope="row" rowspan=2 | A | colspan="2" | anatole | colspan="2" | // | colspan="2" | variation | colspan="2" | ’50s | ½ c. | colspan="3" | anatole | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii}} || <nowiki>|</nowiki> {{Times New Roman|V<sup>7</sup>}} || <nowiki>|</nowiki> |- align="center" ! scope="row" rowspan="2" | A’ | colspan="2" | anatole | colspan="2" | // | colspan="2" | variation | colspan="2" | ’50s | ½ c. | colspan="2" | anatole | c.p. | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> |- align="center" | | colspan="6" | B1 : ''mi'' majeur | colspan="6" background="lightgray" | B2 : ''sol'' majeur | |- align="center" ! scope="row" rowspan="2" | B | colspan="2" | ’50s | colspan="2" | // |colspan="2" | c.p. | colspan="2" | ’50s | colspan="2" | // |colspan="2" | c.p. | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I<sup>7</sup>}} || <nowiki>|</nowiki> |- ! scope="row" | A’ | colspan="12" | | |} === Progression blues === La musique blues est apparue dans les années 1860. Elle est en général bâtie sur une grille d'accords ''({{lang|en|changes}})'' immuable de douze mesures ''({{lang|en|twelve-bar blues}})''. C'est sur cet accompagnement qui se répète que s'ajoute la mélodie — chant et solo. Cette structure est typique du blues et se retrouve dans ses dérivés comme le rock 'n' roll. Le rythme est toujours un rythme ternaire syncopé ''({{lang|en|shuffle, swing, groove}}, ''notes inégales'')'' : la mesure est à quatre temps, mais la noire est divisée en noire-croche en triolet, ou encore triolet de croche en appuyant la première et la troisième. La mélodie se construit en général sur une gamme blues de six degrés (gamme pentatonique mineure avec une quarte augmentée), mais bien que la gamme soit mineure, l'harmonie est construite sur la gamme majeure homonyme : un blues en ''fa'' a une mélodie sur la gamme de ''fa'' mineur, mais une harmonie sur la gamme de ''fa'' majeur. La grille d'accord comporte les accords construits sur les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la gamme majeure homonyme. Les accords sont souvent des accords de septième (donc avec une tierce majeure et une septième mineure), il ne s'agit donc pas d'une harmonisation de gamme diatonique (puisque la septième est majeure sur l'accord de tonique). Par exemple, pour un blues en ''do'' : * accord parfait de do majeur, C ({{Times New Roman|I}}<sup>er</sup> degré) ; * accord parfait de fa majeur, F ({{Times New Roman|IV}}<sup>e</sup> degré) ; * accord parfait de sol majeur, G ({{Times New Roman|V}}<sup>e</sup> degré). Il existe quelques morceaux harmonisés avec des accords mineurs, comme par exemple ''As the Years Go Passing By'' d'Albert King (Duje Records, 1959). La progression blues est organisée en trois blocs de quatre mesures ayant les fonctions suivantes (voir ci-dessus ''[[#Harmonie fonctionnelle|Harmonie fonctionnelle]]'') : * quatre mesures toniques ; * quatre mesures sous-dominantes ; * quatre mesures dominantes. La forme la plus simple, que Jeff Gardner appelle « forme A », est la suivante : {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, forme A |- ! scope="row" | Tonique | width="50px" | I | width="50px" | I | width="50px" | I | width="50px" | I |- ! scope="row" | Sous-domminante | width="50px" | IV | width="50px" | IV | width="50px" | I | width="50px" | I |- ! scope="row" | Dominante | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | V |} La progression {{Times New Roman|I-V}} des deux dernières mesures forment le ''{{lang|en|turnaround}}'', la demie cadence qui lance le cycle suivant. Nous présentons ci-dessous un exemple typique de ligne de basse ''({{lang|en|walking bass}})'' pour le ''{{lang|en|turnaround}}'' d'un blues en ''la'' : [[Fichier:Turnaround classique blues en la.svg|Exemple typique de ligne de basse pour un ''turnaround'' de blues en ''la''.]] [[Fichier:Blues mi harmonie elementaire.midi|thumb|Blues en ''mi'', harmonisé de manière élémentaire avec une ''{{lang|en|walking bass}}''.]] Vous pouvez écouter ci-contre une harmonisation typique d'un blues en ''mi''. Les accords sont exécutés par une basse marchante ''({{lang|en|walking bass}})'', qui joue une arpège sur la triade avec l'ajout d'une sixte majeure et d'une septième mineure, et par une guitare qui joue un accord de puissance ''({{lang|en|power chord}})'', qui n'est composé que de la fondamentale et de la quinte juste, avec une sixte en appoggiature. La forme B s'obtient en changeant la deuxième mesure : on joue un degré {{Times New Roman|IV}} au lieu d'un degré {{Times New Roman|I}}. La progression {{Times New Roman|I-IV}} sur les deux premières mesures est appelé ''{{lang|en|quick change}}''. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, forme B |- | width="50px" | I | width="50px" | IV | width="50px" | I | width="50px" | I |- | width="50px" | IV | width="50px" | IV | width="50px" | I | width="50px" | I |- | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | V |} Par exemple, ''Sweet Home Chicago'' (Robert Johnson, 1936) est un blues en ''fa'' ; sa grille d'accords, aux variations près, suit une forme B : {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression de ''Sweet Home Chicago'' |- | width="50px" | F | width="50px" | B♭ | width="50px" | F | width="50px" | F |- | width="50px" | B♭ | width="50px" | B♭ | width="50px" | F | width="50px" | F |- | width="50px" | C7 | width="50px" | B♭7 | width="50px" | F7 | width="50px" | C7 |} : Écouter {{lien web | url =https://www.youtube.com/watch?v=dkftesK2dck | titre = Robert Johnson "Sweet Home Chicago" | auteur = Michal Angel | site = YouTube | date = 2007-12-09 | consulté le = 2020-12-17 }} Les formes C et D s'obtiennent à partir des formes A et B en changeant le dernier accord par un accord sur le degré {{Times New Roman|I}}, ce qui forme une cadence plagale. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, formes C et D |- | colspan="4" | … |- | colspan="4" | … |- | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | I |} L'harmonie peut être enrichie, notamment en jazz. Voici par exemple une grille du blues souvent utilisés en bebop. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Exemple de progression de blues bebop sur une base de forme B |- | width="60px" | I<sup>7</sup> | width="60px" | IV<sup>7</sup> | width="60px" | I<sup>7</sup> | width="60px" | V–<sup>7</sup> <nowiki>|</nowiki> I<sup>7</sup> |- | width="60px" | IV<sup>7</sup> | width="60px" | IV<sup>7</sup> | width="60px" | I<sup>7</sup> | width="60px" | VI<sup>7 ♯9 ♭13</sup> |- | width="60px" | II–<sup>7</sup> | width="60px" | V<sup>7</sup> | width="60px" | V<sup>7</sup> <nowiki>|</nowiki> IV<sup>7</sup> | width="60px" | II–<sup>7</sup> <nowiki>|</nowiki> V<sup>7</sup> |} On peut aussi trouver des blues sur huit mesures, sur seize mesures comme ''Watermelon Man'' de Herbie Hancock (album ''Takin' Off'', Blue Note, 1962) ou ''Let's Dance'' de Jim Lee (interprété par Chris Montez, Monogram, 1962) * {{lien web |url= https://www.dailymotion.com/video/x5iduwo |titre=Herbie Hancock - Watermelon Man (1962) |auteur=theUnforgettablesTv |site=Dailymotion |date=2003 |consulté le=2021-02-09 }} * {{lien web |url=https://www.youtube.com/watch?v=6JXshurYONc |titre=Let's Dance |auteur=Chris Montez |site=YouTube |date=2016-08-06 |consulté le=2021-02-09 }} À l'inverse, certains blues peuvent avoir une structure plus simple que les douze mesure ; par exemple ''Hoochie Coochie Man'' de Willie Dixon (interprété par Muddy Waters sous le titre ''Mannish Boy'', Chicago Blues, 1954) est construit sur un seul accord répété tout le long de la chanson. * {{lien web |url=https://www.dailymotion.com/video/x5iduwo |titre=Muddy Waters - Hoochie Coochie Man |auteur=Muddy Waters |site=Dailymotion |date=2012 | consulté le=2021-02-09 }} === Cadence andalouse === La cadence andalouse est une progression de quatre accords, descendant par mouvement conjoint : * en mode de ''mi'' (mode phrygien) : {{Times New Roman|IV}} - {{Times New Roman|III}} - {{Times New Roman|II}} - {{Times New Roman|I}} ;<br />par exemple en ''mi'' phrygien : Am - G - F - E ; en ''do'' phrygien : Fm - E♭ - D♭ - C ;<br />on notera que le degré {{Times New Roman|III}} est diésé dans l'accord final (ou bécarre s'il est bémol dans la tonalité) ; * en mode mineur : {{Times New Roman|I}} - {{Times New Roman|VII}} - {{Times New Roman|VI}} - {{Times New Roman|V}} ;<br />par exemple en ''la'' mineur : Am - G - F - E ; en ''do'' mineur : Cm - B♭ - A♭ - m ;<br />comme précédemment, on notera que le degré {{Times New Roman|VII}} est diésé dans l'accord final. === Progressions selon le cercle des quintes === [[Fichier:Cercle quintes degres tonalite majeure.svg|vignette|Cercle des quinte justes (parcouru dans le sens des aiguilles d'une montre) des degrés d'une tonalité majeure.]] La progression {{Times New Roman|V-I}} est la cadence parfaite, mais on peut aussi l'employer au milieu d'un morceau. Cette progression étant courte, sa répétition crée de la lassitude ; on peut la compléter par d'autres accords séparés d'une quinte juste, en suivant le « cercle des quintes » : {{Times New Roman|I-V-IX}}, la neuvième étant enharmonique de la seconde, on obtient {{Times New Roman|I-V-II}} ; en retournant la progression, on obtient le turnaround {{Times New Roman|II-V-I}}. En jazz, dans le morceau ''Topsy'' (Edgar Battle et Eddir Durham), la partie <span style="border-color: black; border-style: solid; border-width: thin;">C</span> est composée d'une progression en quintes descendantes d'accords de septième de dominante : D<sup>7</sup> - G<sup>7</sup> - C<sup>7</sup> - F<sup>7</sup> - B♭<sup>7</sup> ; les improvisatiosn sur cette partie peuvent donc se faire en modulant en ''sol'' (majeur ou mineur) - ''do'' (M/m) - ''fa'' (M/m) - ''si''♭ (M/m) - ''mi''♭ (M/m). On peut continuer de décrire le cercle des quintes : {{Times New Roman|I-V-II-VI}}, on obtient l'anatole dans le désordre ; on peut à l'inverse étendre les quintes vers la gauche, {{Times New Roman|IV-I-V-II-VI}}. En musique populaire, on trouve fréquemment une progression fondée sur les accord {{Times New Roman|I}}, {{Times New Roman|IV}}, {{Times New Roman|V}} et {{Times New Roman|VI}}, popularisée dans les années 1950. La « progression années 1950 », « progression ''{{lang|en|fifties ('50)}}'' » ''({{lang|en|'50s progression}})'' est dans l'ordre {{Times New Roman|I-VI-IV-V}}. On trouve aussi cette progression en musique classique. Si la tonalité est majeure, la triade sur la sus-dominante est mineure, les autres sont majeures, on notera donc souvent {{Times New Roman|I-vi-IV-V}}. On peut avoir des permutations circulaires (le dernier accord venant au début, ou vice-versa) : {{Times New Roman|vi-IV-V-I}}, {{Times New Roman|IV-V-I-vi}} et {{Times New Roman|V-I-vi-IV}}. {| class="wikitable" |+ Accords selon la tonalité ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" style="font-family:Times New Roman" | I ! scope="col" style="font-family:Times New Roman" | IV ! scope="col" style="font-family:Times New Roman" | V ! scope="col" style="font-family:Times New Roman" | vi |- |''Do'' majeur || || C || F || G || Am |- |''Sol'' majeur || ''fa''♯ || G || C || D || Em |- |''Ré'' majeur || ''fa''♯, ''do''♯ || D || G || A || Bm |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A || D || E || F♯m |- | ''Fa'' majeur || ''si''♭ || F || B♭ || C || Dm |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭ || E♭ || F || Gm |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭ || A♭ || B♭ || Cm |} Par exemple, en tonalité de ''do'' majeur, la progression {{Times New Roman|I-vi-IV-V}} sera C-Am-F-G. Il existe d'autres progressions utilisant ces accords mais dans un autre ordre, typiquement {{Times New Roman|I–IV–vi–V}} ou une de ses permutations circulaires : {{Times New Roman|IV–vi–V-I}}, {{Times New Roman|vi–V-I-IV}} ou {{Times New Roman|V-I-IV-vi}}. Ou dans un autre ordre. PV Nova l'illustre dans plusieurs de ses « expériences » dans la version {{Times New Roman|vi-V-IV-I}}, soit Am-G-F-C, ou encore {{Times New Roman|vi-IV-I-V}}, soit Am-F-C-G : : {{lien web | url = https://www.youtube.com/watch?v=w08LeZGbXq4 | titre = Expérience n<sup>o</sup> 6 — La Happy Pop | auteur = PV Nova | site = YouTube | date = 2011-08-20 | consulté le = 2020-12-13 }} et cela devient un gag récurrent avec son « chapeau des accords magiques qu'on nous ressort à toutes les sauces » : {{lien web | url = https://www.youtube.com/watch?v=VMY_vc4nZAU | titre = Expérience n<sup>o</sup> 14 — La Soupe dou Brasil | auteur = PV Nova | site = YouTube | date = 2012-10-03 | consulté le = 2020-12-17 }} Cette récurrence est également parodiée par le groupe The Axis of Awesome avec ses « chansons à quatre accords » ''({{lang|en|four-chords song}})'', dans une sketch où ils mêlent 47 chansons en utilisant l'ordre {{Times New Roman|I-V-vi-IV}} : : {{lien web | url = https://www.youtube.com/watch?v=oOlDewpCfZQ | titre = 4 Chords | Music Videos | The Axis Of Awesome | auteur = The Axis of Awesome | site = YouTube | date = 2011-07-20 | consulté le = 2020-12-17 }} {{boîte déroulante/début|titre=Chansons mêlées dans le sketch}} # Journey : ''Don't Stop Believing'' ; # James Blunt : ''You're Beautiful'' ; # Black Eyed Peas : ''Where Is the Love'' ; # Alphaville : ''Forever Young'' ; # Jason Mraz : ''I'm Yours'' ; # Train : ''Hey Soul Sister'' ; # The Calling : ''Wherever You Will Go'' ; # Elton John : ''Can You Feel The Love Tonight'' (''Le Roi lion'') ; # Akon : ''Don't Matter'' ; # John Denver : ''Take Me Home, Country Roads'' ; # Lady Gaga : ''Paparazzi'' ; # U2 : ''With Or Without You'' ; # The Last Goodnight : ''Pictures of You'' ; # Maroon Five : ''She Will Be Loved'' ; # The Beatles : ''Let It Be'' ; # Bob Marley : ''No Woman No Cry'' ; # Marcy Playground : ''Sex and Candy'' ; # Men At Work : ''Land Down Under'' ; # thème de ''America's Funniest Home Videos'' (équivalent des émissions ''Vidéo Gag'' et ''Drôle de vidéo'') ; # Jack Johnson : ''Taylor'' ; # Spice Girls : ''Two Become One'' ; # A Ha : ''Take On Me'' ; # Green Day : ''When I Come Around'' ; # Eagle Eye Cherry : ''Save Tonight'' ; # Toto : ''Africa'' ; # Beyonce : ''If I Were A Boy'' ; # Kelly Clarkson : ''Behind These Hazel Eyes'' ; # Jason DeRulo : ''In My Head'' ; # The Smashing Pumpkins : ''Bullet With Butterfly Wings'' ; # Joan Osborne : ''One Of Us'' ; # Avril Lavigne : ''Complicated'' ; # The Offspring : ''Self Esteem'' ; # The Offspring : ''You're Gonna Go Far Kid'' ; # Akon : ''Beautiful'' ; # Timberland featuring OneRepublic : ''Apologize'' ; # Eminem featuring Rihanna : ''Love the Way You Lie'' ; # Bon Jovi : ''It's My Life'' ; # Lady Gaga : ''Pokerface'' ; # Aqua : ''Barbie Girl'' ; # Red Hot Chili Peppers : ''Otherside'' ; # The Gregory Brothers : ''Double Rainbow'' ; # MGMT : ''Kids'' ; # Andrea Bocelli : ''Time To Say Goodbye'' ; # Robert Burns : ''Auld Lang Syne'' ; # Five for fighting : ''Superman'' ; # The Axis of Awesome : ''Birdplane'' ; # Missy Higgins : ''Scar''. {{boîte déroulante/fin}} Vous pouvez par exemple jouer les accords C-G-Am-F ({{Times New Roman|I-V-vi-IV}}) et chanter dessus ''{{lang|en|Let It Be}}'' (Paul McCartney, The Beattles, 1970) ou ''Libérée, délivrée'' (Robert Lopez, ''La Reine des neiges'', 2013). La progression {{Times New Roman|I-V-vi-IV}} est considérée comme « optimiste » tandis que sa variante {{Times New Roman|iv-IV-I-V}} est considérée comme « pessimiste ». On peut voir la progression {{Times New Roman|I-vi-IV-V}} comme une variante de l'anatole {{Times New Roman|I-vi-ii-V}}, obtenue en remplaçant l'accord de sustonique {{Times New Roman|ii}} par l'accord de sous-dominante {{Times New Roman|IV}} (son relatif majeur, et degré ayant la même fonction). ==== Exemples de progression selon le cercle des quintes en musique classique ==== [[Fichier:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.midi|vignette|Dietrich Buxtehude, Psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]] Cette progression selon la cercle des quintes, sous la forme {{Times New Roman|I-vi-IV-V}}, apparaît déjà au {{pc|xvii}}<sup>e</sup> siècle dans le psaume 42 ''Quem ad modum desiderat cervis'' (BuxVW92) de Dietrich Buxtehude (1637-1707). Le morceau est en ''fa'' majeur, la progression d'accords est donc F-Dm-B♭-C. : {{lien web | url = https://www.youtube.com/watch?v=8FmV9l1RqSg | titre = D. Buxtehude - Quemadmodum desiderat cervus, BuxWV 92 | auteur = Longobardo | site = YouTube | date = 2013-04-06 | consulté la = 2021-01-01 }} [[File:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.svg|vignette|450x450px|center|Dietrich Buxtehude, psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]] {{clear}} [[Fichier:JSBach BWV140 cantate 4 mesures.midi|vignette|J.-S. Bach, cantate BWV140, quatre premières mesures.]] On la trouve également dans l'ouverture de la cantate ''{{lang|de|Wachet auf, ruft uns die Stimme}}'' de Jean-Sébastien Bach (BWV140, 1731). Le morceau est en ''mi''♭ majeur, la progression d'accords est donc E♭-Cm-A♭<sup>6</sup>-B♭. [[Fichier:JSBach BWV140 cantate 4 mesures.svg|vignette|center|J.-S. Bach, cantate BWV140, quatre premières mesures.|alt=|517x517px]] {{clear}} [[Fichier:Mozart K310 Sonate8 mesures 45 a 49.midi|vignette|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]] La même progression est utilisée par Mozart, par exemple dans le premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778), la progression d'accords est C-Am-F-G qui correspond à la progression {{Times New Roman|III-i-VI-VII}} de ''la'' mineur, mais à la progression {{Times New Roman|I-vi-IV-V}} de la gamme relative, ''do'' majeur . [[Fichier:Mozart K310 Sonate8 mesures 45 a 49.svg|vignette|center|500px|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]] === Substitution tritonique === Un des accords les plus utilisés est donc l'accord de septième de dominante, {{Times New Roman|V<sup>7</sup><sub>+</sub>}} qui contient les degrés {{Times New Roman|V}}, {{Times New Roman|VII}}, {{Times New Roman|II}} ({{Times New Roman|IX}}) et {{Times New Roman|IV}}({{Times New Roman|XI}}) ; par exemple, en tonalité de ''do'' majeur, l'accord de ''sol'' septième (G<sup>7</sup>) contient les notes ''sol''-''si''-''ré''-''fa''. Si l'on prend l'accord dont la fondamentale est trois tons (triton) au-dessus ou en dessous — l'octave contenant six tons, on arrive sur la même note —, {{Times New Roman|♭II<sup>7</sup>}}, ici ''ré''♭ septième (D♭<sup>7</sup>), celui-ci contient les notes ''ré''♭-''fa''-''la''♭-''do''♭, cette dernière note étant l'enharmonique de ''si''. Les deux accords G<sup>7</sup> et D♭<sup>7</sup> ont donc deux notes en commun : le ''fa'' et le ''si''/''do''♭. Il est donc fréquent en jazz de substituer l'accord {{Times New Roman|V<sup>7</sup><sub>+</sub>}} par l'accord {{Times New Roman|♭II<sup>7</sup>}}. Par exemple, la progression {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|V<sup>7</sup>}}-{{Times New Roman|I<sup>&Delta;</sup>}} devient {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|♭II<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}}. C'est un procédé courant de réharmonisation (le fait de remplacer un accord par un autre dans un morceau existant). Les six substitutions possibles sont donc : C<sup>7</sup>↔F♯<sup>7</sup> - D♭<sup>7</sup>↔G<sup>7</sup> - D<sup>7</sup>↔A♭<sup>7</sup> - E♭<sup>7</sup>↔A<sup>7</sup> - E<sup>7</sup>↔B♭<sup>7</sup> - F<sup>7</sup>↔B<sup>7</sup>. [[Fichier:Übermäsiger Terzquartakkord.jpg|vignette|Exemple de cadence parfaite en ''do'' majeur avec substitution tritonique (sixte française).]] Dans l'accord D♭<sup>7</sup>, si l'on remplace le ''do''♭ par son ''si'' enharmonique, on obtient un accord de sixte augmentée : ''ré''♭-''fa''-''la''♭-''si''. Cet accord est utilisé en musique classique depuis la Renaissance ; on distingue en fait trois accords de sixte augmentée : * sixte française ''ré''♭-''fa''-''sol''-''si'' ; * sixte allemande : ''ré''♭-''fa''-''la''♭-''si'' ; * sixte italienne : ''ré''♭-''fa''-''si''. Par exemple, le ''Quintuor en ''ut'' majeur'' de Franz Schubert (1828) se termine par une cadence parfaite dont l'accord de dominante est remplacé par une sixte française ''ré''♭-''fa''-''si''-''sol''-''si'' (''ré''♭ aux violoncelles, ''fa'' à l'alto, ''si''-''sol'' aux seconds violons et ''si'' au premier violon). [[Fichier:Schubert C major Quintet ending.wav|vignette|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]] [[Fichier:Schubert C major Quintet ending.png|vignette|center|upright=2.5|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]] === Autres accords de substitution === Substituer un accord consiste à utiliser un accord provenant d'une tonalité étrangère à la tonalité en cours. À la différence d'une modulation, la substitution est très courte et ne donne pas l'impression de changer de tonalité ; on a juste un sentiment « étrange » passager. Un court passage dans une autre tonalité est également appelée « emprunt ». Nous avons déjà vu plusieurs méthodes de substitution : * utilisation d'une note étrangère : une note étrangère — note de passage, appoggiature, anticipation, retard… — crée momentanément un accord hors tonalité ; en musique classique, ceci n'est pas considéré comme un accord en propre, mais en jazz, on parle « d'accord de passage » et « d'accord suspendu » ; * utilisation d'une dominante secondaire : l'accord de dominante secondaire est hors tonalité ; le but ici est de faire une cadence parfaite, mais sur un autre degré que la tonique de la tonalité en cours ; * la substitution tritonique, vue ci-dessus, pour remplacer un accord de septième de dominante. Une dernière méthode consiste à remplacer un accord par un accord d'une gamme de même tonique, mais d'un autre mode ; on « emprunte » ''({{lang|en|borrow}})'' l'accord d'un autre mode. Par exemple, substituer un accord de la tonalité de ''do'' majeur par un accord de la tonalité de ''do'' mineur ou de ''do'' mode de ''mi'' (phrygien). Donc en ''do'' majeur, on peut remplacer un accord de ''ré'' mineur septième (D<sub>m</sub><sup>7</sup>) par un accord de ''ré'' demi-diminué (D<sup>⌀</sup>, D<sub>m</sub><sup>7&flat;5</sup>) qui est un accord appartenant à la donalité de ''la'' mineur harmonique. === Forme AABA === La forme AABA est composée de deux progressions de huit mesures, notées A et B ; cela représente trente-deux mesures au total, on parle donc souvent en anglais de la ''{{lang|en|32-bars form}}''. C'est une forme que l'on retrouve dans de nombreuses chanson de comédies musicales de Broadway comme ''Have You Met Miss Jones'' (''{{lang|en|I'd Rather Be Right}}'', 1937), ''{{lang|en|Over the Rainbow}}'' (''Le Magicien d'Oz'', Harold Harlen, 1939), ''{{lang|en|All the Things You Are}}'' (''{{lang|en|Very Warm for may}}'', 1939). Par exemple, la version de ''{{lang|en|Over the Rainbow}}'' chantée par Judy Garland est en ''la''♭ majeur et la progression d'accords est globalement : * A (couplet) : A♭-Fm | Cm-A♭ | D♭ | Cm-A♭ | D♭ | D♭-F | B♭-E♭ | A♭ * B (pont) : A♭ | B♭m | Cm | D♭ | A♭ | B♭-G | Cm-G | B♭m-E♭ soit en degrés : * A : {{Times New Roman|<nowiki>I-vi | iii-I | IV | iii-IV | IV | IV-vi | II-V | I</nowiki>}} * B : {{Times New Roman|<nowiki>I | ii | iii | IV | I | II-VII | iii-VII | ii-V</nowiki>}} Par rapport aux paroles de la chanson, on a * A : couplet 1 ''« {{lang|en|Somewhere […] lullaby}} »'' ; * A : couplet 2 ''« {{lang|en|Somewhere […] really do come true}} »'' ; * B : pont ''« {{lang|en|Someday […] you'll find me}} »'' ; * A : couplet 3 ''« {{lang|en|Somewhere […] oh why can't I?}} »'' ; : {{lien web | url = https://www.youtube.com/watch?v=1HRa4X07jdE | titre = Judy Garland - Over The Rainbow (Subtitles) | site = YouTube | auteur = Overtherainbow | consulté le = 2020-12-17 }} Une mise en œuvre de la forme AABA couramment utilisée en jazz est la forme anatole (à le pas confondre avec la succession d'accords du même nom), en anglais ''{{lang|en|rythm changes}}'' car elle s'inspire du morceau ''{{lang|en|I Got the Rythm}}'' de George Gerschwin (''Girl Crazy'', 1930) : * A : {{Times New Roman|I–vi–ii–V}} (succession d'accords « anatole ») ; * B : {{Times New Roman|III<sup>7</sup>–VI<sup>7</sup>–II<sup>7</sup>–V<sup>7</sup>}} (les fondamentales forment une succession de quartes, donc parcourent le « cercle des quintes » à l'envers). Par exemple, ''I Got the Rythm'' étant en ''ré''♭ majeur, la forme est : * A : D♭ - B♭m - E♭m - A♭ * B : F7 - B♭7 - E♭7 - A♭7 === Exemples === ==== Début du Largo de la symphonie du Nouveau Monde ==== [[File:Largo nouveau monde 5 1res mesures.svg|vignette|Partition avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]] [[File:Largo nouveau monde 5 1res mesures.midi|vignette|Fichier son avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]] Nous avons reproduit ci-contre les cinq premières mesure du deuxième mouvement Largo de la symphonie « Du Nouveau Monde » (symphonie n<sup>o</sup> 9 d'Antonín Dvořák, 1893). Cliquez sur l'image pour l'agrandir. Vous pouvez écouter cette partie jouée par un orchestre symphonique : * {{lien web |url =https://www.youtube.com/watch?v=y2Nw9r-F_yQ?t=565 |titre = Dvorak Symphony No.9 "From the New World" Karajan 1966 |site=YouTube (Seokjin Yoon) |consulté le=2020-12-11 }} (à 9 min 25), par le Berliner Philharmoniker, dirigé par Herbert von Karajan (1966) ; * {{lien web |url = https://www.youtube.com/watch?v=ASlch7R1Zvo |titre=Dvořák: Symphony №9, "From The New World" - II - Largo |site=YouTube (diesillamusicae) |consulté le=2020-12-11 }} : Wiener Philharmoniker, dirigé par Herbert von Karajan (1985). {{clear}} Cette partie fait intervenir onze instruments monodiques (ne jouant qu'une note à la fois) : des vents (trois bois, sept cuivres) et une percussion. Certains de ces instruments sont transpositeurs (les notes sur la partition ne sont pas les notes entendues). Jouées ensemble, ces onze lignes mélodiques forment des accords. Pour étudier cette partition, nous réécrivons les parties des instruments transpositeurs en ''do'' et les parties en clef d’''ut'' en clef de ''fa''. Nous regroupons les parties en clef de ''fa'' d'un côté et les parties en clef de ''sol'' d'un autre. {{boîte déroulante|Résultat|contenu=[[File:Largo nouveau monde 5 1res mesures transpositeurs en do.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde, en do.]]}} Nous pouvons alors tout regrouper sous la forme d'un système de deux portées clef de ''fa'' et clef de ''sol'', comme une partition de piano. {{boîte déroulante|Résultat|contenu= [[Fichier:Largo nouveau monde 5 1res mesures accords.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords.]] }} [[Fichier:Largo nouveau monde 5 1res mesures accords.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords.]] {{clear}} Ensuite, nous ne gardons que la basse et les notes médium. Nous changeons éventuellement certaines notes d'octave afin de n'avoir que des superpositions de tierce ou de quinte (état fondamental des accords, en faisant ressortir les notes manquantes). {{boîte déroulante|Résultat|contenu= [[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords simplifiés.]] }} [[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords simplifiés.]] Vous pouvez écouter cette partie jouée par un quintuor de cuivres (trompette, bugle, cor, trombone, tuba), donc avec des accords de cinq notes : : {{lien web |url=https://www.youtube.com/watch?v=pWfe60nbvjA |titre = Largo from The New World Symphony by Dvorak |site=YouTube (The Chamberlain Brass) |consulté le=2020-12-11 }} : The American Academy of Arts & Letters in New York City (2017). Nous allons maintenant chiffrer les accords. Pour établir la basse chiffrée, il nous faut déterminer le parcours harmonique. Pour le premier accord, les tonalités les plus simples avec un ''sol'' dièse sont ''la'' majeur et ''fa'' dièse mineur ; comme le ''mi'' est bécarre, nous retenons ''la'' majeur, il s'agit donc d'un accord de quinte sur la dominante (les accords de dominante étant très utilisés, cela nous conforte dans notre choix). Puis nous avons un ''si'' bémol, nous pouvons être en ''fa'' majeur ou en ''ré'' mineur ; nous retenons ''fa'' majeur, c'est donc le renversement d'un accord sur le degré {{Times New Roman|II}}. Dans la deuxième mesure, nous revenons en ''la'' majeur, puis, avec un ''la'' et un ''ré'' bémols, nous sommes en ''la'' bémol majeur ; nous avons donc un accord de neuvième incomplet sur la sensible, ou un accord de onzième incomplet sur la dominante. Dans la troisième mesure, nous passons en ''ré'' majeur, avec un accord de dominante. Puis, nous arrivons dans la tonalité principale, avec le renversement d'un accord de dominante sans tierce suivi d'un accord de tonique. Nous avons donc une cadence parfaite, conclusion logique d'une phrase. La progression des accords est donc : {| class="wikitable" ! scope="row" | Tonalité | ''la'' M - ''fa'' M || ''la'' M - ''la''♭ M || ''ré'' M - ''ré''♭ M || ''ré''♭ M |- ! scope="row" | Accords | {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|II}}<sup>6</sup><sub>4</sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|“V”}}<sup>9</sup><sub><s>5</s></sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|V}}<sup>+4</sup> || {{Times New Roman|I}}<sup>5</sup> |} Dans le chiffrage jazz, nous avons donc : * une triade de ''mi'' majeur, E ; * une triade de ''sol'' majeur avec un ''ré'' en basse : G/D ; * à nouveau un E ; * un accord de ''sol'' neuvième diminué incomplet, avec un ''ré'' bémol en basse : G dim<sup>9</sup>/D♭ ; * un accord de ''la'' majeur, A ; * un accord de ''la'' bémol septième avec une ''sol'' bémol à la basse : A♭<sup>7</sup>/G♭ ; * la partie se conclue par un accord parfait de ''ré''♭ majeur, D♭. Soit une progression E - G/D | E - G dim<sup>9</sup>/D♭ | A - A♭<sup>7</sup>/G♭ | D♭. [[Fichier:Largo nouveau monde 5 1res mesures accords chiffres.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde en accords simplifiés.]] {{clear}} ==== Thème de Smoke on the Water ==== Le morceau ''Smoke on the Water'' du groupe Deep Purple (album ''Machine Head'', 1972) possède un célèbre thème, un riff ''({{lang|en|rythmic figure}})'', joué à la guitare sous forme d'accords de puissance ''({{lang|en|power chords}})'', c'est-à-dire des accords sans tierce. Le morceau est en tonalité de ''sol'' mineur naturel (donc avec un ''fa''♮) avec ajout de la note bleue (''{{lang|en|blue note}}'', quinte diminuée, ''ré''♭), et les accords composant le thème sont G<sup>5</sup>, B♭<sup>5</sup>, C<sup>5</sup> et D♭<sup>5</sup>, ce dernier accord étant l'accord sur la note bleue et pouvant être considéré comme une appoggiature (indiqué entre parenthèse ci-après). On a donc ''a priori'', sur les deux premières mesures, une progression {{Times New Roman|I-III-IV}} puis {{Times New Roman|I-III-(♭V)-IV}}. Durant la majeure partie du thème, la guitare basse tient la note ''sol'' en pédale. {{note|En jazz, la qualité « <sup>5</sup> » indique que l'on n'a que la quinte (et donc pas la tierce), contrairement à la notation de basse chiffrée.}} : {{lien web | url = https://www.dailymotion.com/video/x5ili04 | titre = Deep Purple — Smoke on the Water (Live at Montreux 2006) | auteur = Deep Purple | site = Dailymotion | date = 2016 | consulté le = 2020-12-31 }} Cependant, cette progression forme une mélodie, on peut donc plus la voir comme un contrepoint, la superposition de deux voies ayant un mouvement conjoint, joué par un seul instrument, la guitare, la voie 2 étant jouée une quarte juste en dessous de la voie 1 (la quarte juste descendante étant le renversement de la quinte juste ascendante) : * voie 1 (aigu) : | ''sol'' - ''si''♭ - ''do'' | ''sol'' - ''si''♭ - (''ré''♭) - ''do'' | ; * voie 2 (grave) : | ''ré'' - ''fa'' - ''sol'' | ''ré'' - ''fa'' - (''la''♭) - ''sol'' |. En se basant sur la basse (''sol'' en pédale), nous pouvons considérer que ces deux mesures sont accompagnées d'un accord de Gm<sup>7</sup> (''sol''-''si''♭-''ré''-''fa''), chaque accord de la mélodie comprenant à chaque fois au moins une note de cet accord à l'exception de l'appogiature. {| class="wikitable" |+ Mise en évidence des notes de l'accord Gm<sup>7</sup> |- ! scope="row" | Accords | G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> |- ! scope="row" | Voie 1 | '''''sol''''' || '''''si''♭''' || ''do'' |- ! scope="row" | Voie 2 | '''''ré''''' || '''''fa''''' || '''''sol''''' |- ! scope="row" | Basse | '''''sol''''' || '''''sol''''' || '''''sol''''' |} Sur les deux mesures suivantes, la basse varie et suit les accords de la guitare avec un retard sur le dernier accord : {| class="wikitable" |+ Voies sur les mesure 3-4 du thème |- ! scope="row" | Accords | G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> || B♭<sup>5</sup> || G<sup>5</sup> |- ! scope="row" | Voie 1 | ''sol'' || ''si''♭ || ''do'' || ''si''♭ || ''sol'' |- ! scope="row" | Voie 2 | ''ré'' || ''fa'' || ''sol'' || ''fa'' || ''ré'' |- ! scope="row" | Basse | ''sol'' || ''sol'' || ''do'' || ''si''♭ || ''si''♭-''sol'' |} Le couplet de cette chanson est aussi organisé sur une progression de quatre mesures, la guitare faisant des arpèges sur les accords G<sup>5</sup> (''sol''-''ré''-''sol'') et F<sup>5</sup> (''fa''-''do''-''fa'') : : | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-F<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | soit une progression {{Times New Roman|<nowiki>| I-I | I-I | I-VII | I-I |</nowiki>}}. Nous pouvons aussi harmoniser le riff du thème sur cette progression, avec un accord F (''fa''-''la''-''do'') ; nous pouvons aussi nous rappeler que l'accord sur le degré {{Times New Roman|VII}} est plus volontiers considéré comme un accord de septième de dominante {{Times New Roman|V<sup>7</sup>}}, soit ici un accord Dm<sup>7</sup> (''ré''-''fa''-''la''-''do''). On peut donc considérer la progression harmonique sur le thème : : | Gm-Gm | Gm-Gm | Gm-F ou Dm<sup>7</sup> | Gm-Gm |. Cette analyse permet de proposer une harmonisation enrichie du morceau, tout en se rappelant qu'une des forces du morceau initial est justement la simplicité de sa structure, qui fait ressortir la virtuosité des musiciens. Nous pouvons ainsi comparer la version album à la version concert avec orchestre ou à la version latino de Pat Boone. À l'inverse, le groupe Psychostrip, dans une version grunge, a remplacé les accords par une ligne mélodique : * le thème ne contient plus qu'une seule voie (la guitare ne joue pas des accords de puissance) ; * dans les mesures 9 et 10, la deuxième guitare joue en contrepoint de type mouvement inverse, qui est en fait la voie 2 jouée en miroir ; * l'arpège sur le couplet est remplacé par une ligne mélodique en ostinato sur une gamme blues. {| class="wikitable" |+ Contrepoint sur les mesures 9 et 10 |- ! scope="row" | Guitare 1 | ''sol'' ↗ ''si''♭ ↗ ''do'' |- ! scope="row" | Guitare 2 | ''sol'' ↘ ''fa'' ↘ ''ré'' |} * {{lien web | url = https://www.dailymotion.com/video/x5ik234 | titre = Deep Purple — Smoke on the Water (In Concert with the London Symphony Orchestra, 1999) | auteur = Deep Purple | site = Dailymotion | date = 2016 | consulté le = 2020-12-31 }} * {{lien web | url = https://www.youtube.com/watch?v=MtUuNzVROIg | titre = Pat Boone — Smoke on the Water (In a Metal Mood, No More Mr. Nice Guy, 1997) | auteur = Orrore a 33 Giri | site = YouTube | date = 2019-06-24 | consulté le = 2020-12-31 }} * {{lien web | url = https://www.youtube.com/watch?v=n7zLlZ8B0Bk | titre = Smoke on the Water (Heroes, 1993) | auteur = Psychostrip | site = YouTube | date = 2018-06-20 | consulté le = 2020-12-31 }} == Accords et improvisation == Nous avons vu précédemment (chapitre ''[[../Gammes et intervalles#Modes et improvisation|Gammes et intervalles &gt; Modes et improvisation]]'') que le choix d'un mode adapté permet d'improviser sur un accord. L'harmonisation des gammes permet, en inversant le processus, d'étendre notre palette : il suffit de repérer l'accord sur une harmonisaiton de gamme, et d'utiliser cette gamme-là, dans le mode correspondant du degré de l'accord (voir ci-dessus ''[[#Harmonisation par des accords de septième|Harmonisation par des accords de septième]]''). Par exemple, nous avons vu que l'accord sur le septième degré d'une gamme majeure était un accord demi-diminué ; nous savons donc que sur un accord demi-diminué, nous pouvons improviser sur le mode correspondant au septième degré, soit le mode de ''si'' (locrien). Un accord de septième de dominante étant commun aux deux tonalités homonymes (par exemple ''fa'' majeur et ''fa'' mineur pour un ''do''<sup>7</sup><sub>+</sub> / C<sup>7</sup>), nous pouvons utiliser le mode de ''sol'' de la gamme majeure (mixolydien) ou de la gamme mineure mineure (mode phrygien dominant, ou phrygien espagnol) pour improviser. Mais l'accord de septième de dominante est aussi l'accord au début d'une grille blues ; on peut donc improviser avec une gamme blues, même si la tierce est majeure dans l'accord et mineure dans la gamme. [[Fichier:Mode improvisation accords do complet.svg]] == Autres accords courants == [[fichier:Cluster cdefg.png|vignette|Agrégat ''do - ré - mi - fa - sol''.]] Nous avons vu précédemment l'harmonisation des tonalités majeures et mineures harmoniques par des triades et des accords de septième ; certains accords étant rarement utilisés (l'accord sur le degré {{Times New Roman|III}} et, pour les tonalités mineures harmoniques, l'accord sur la tonique), certains accords étant utilisés comme des accords sur un autre degré (les accords sur la sensible étant considérés comme des accords de dominante sans fondamentale). Dans l'absolu, on peut utiliser n'importe quelle combinaison de notes, jusqu'aux agrégats, ou ''{{lang|en|clusters}}'' (mot anglais signifiant « amas », « grappe ») : un ensemble de notes contigües, séparées par des intervalles de seconde. Dans la pratique, on reste souvent sur des accords composés de superpositions de tierces, sauf dans le cas de transitions (voir la section ''[[#Notes étrangères|Notes étrangère]]''). === En musique classique === On utilise parfois des accords dont les notes ne sont pas dans la tonalité (hors modulation). Il peut s'agir d'accords de passage, de notes étrangères, par exemple utilisant un chromatisme (mouvement conjoint par demi-tons). Outre les accords de passage, les autres accords que l'on rencontre couramment en musique classique sont les accords de neuvième, et les accords de onzième et treizième sur tonique. Ces accords sont simplement obtenus en continuant à empiler les tierces. Il n'y a pas d'accord d'ordre supérieur car la quinzième est deux octaves au-dessus de la fondamentale. Comme pour les accords de septième, on distingue les accords de neuvième de dominante et les accords de neuvième d'espèce. Dans le cas de la neuvième de dominante, il y a une différence entre les tonalités majeures et mineures : l'intervalle de neuvième est respectivement majeur et mineur. Les chiffrages des renversements peuvent donc différer. Comme pour les accords de septième de dominante, on considère que les accords de septième sur le degré {{Times New Roman|VI}} sont en fait des accords de neuvième de dominante sans fondamentale. Les accords de neuvième d'espèce sont en général préparés et résolus. Préparés : la neuvième étant une note dissonante (c'est à une octave près la seconde de la fondamentale), l'accord qui précède doit contenir cette note, mais dans un accord consonant ; la neuvième est donc commune avec l'accord précédent. Résolus : la dissonance est résolue en abaissant la neuvième par un mouvement conjoint. Par exemple, en tonalité de ''do'' majeur, si l'on veut utiliser un accord de neuvième d'espèce sur la tonique ''(do - mi - sol - si - ré)'', on peut utiliser avant un accord de dominante ''(sol - si - ré)'' en préparation puis un accord parfait sur le degré {{Times New Roman|IV}} ''(fa - la - do)'' en résolution ; nous avons donc sur la voie la plus aigüe la succession ''ré'' (consonant) - ''ré'' (dissonant) - ''do'' (consonant). On rencontre également parfois des accords de onzième et de treizième. On omet en général la tierce, car elle est dissonante avec la onzième. L'accord le plus fréquemment rencontré est l'accord sur la tonique : on considère alors que c'est un accord sur la dominante que l'on a enrichi « par le bas », en ajoutant une quinte inférieure. par exemple, dans la tonalité de ''do'' majeur, l'accord ''do - sol - si - ré - fa'' est considéré comme un accord de septième de dominante sur tonique, le degré étant noté « {{Times New Roman|V}}/{{Times New Roman|I}} ». De même pour l'accord ''do - sol - si - ré - fa - la'' qui est considéré comme un accord de neuvième de dominante sur tonique. === En jazz === En jazz, on utilise fréquemment l'accord de sixte à la place de l'accord de septième majeure sur la tonique. Par exemple, en ''do'' majeur, on utilise l'accord C<sup>6</sup> ''(do - mi - sol - la)'' à la place de C<sup>Δ</sup> ''(do - mi - sol - si)''. On peut noter que C<sup>6</sup> est un renversement de Am<sup>7</sup> et pourrait donc se noter Am<sup>7</sup>/C ; cependant, le fait de le noter C<sup>6</sup> indique que l'on a bien un accord sur la tonique qui s'inscrit dans la tonalité de ''do'' majeur (et non, par exemple, de ''la'' mineur naturelle) — par rapport à l'harmonie fonctionnelle, on remarquera que Am<sup>7</sup> a une fonction tonique, l'utilisation d'un renversement de Am<sup>7</sup> à la place d'un accord de C<sup>Δ</sup> est donc logique. Les accords de neuvième, onzième et treizième sont utilisés comme accords de septième enrichis. Le chiffrage suit les règles habituelles : on ajoute un « 9 », un « 11 » ou un « 13 » au chiffrage de l'accord de septième. On utilise également des accords dits « suspendus » : ce sont des accords de transition qui sont obtenus en prenant une triade majeure ou mineure et en remplaçant la tierce par la quarte juste (cas le plus fréquent) ou la seconde majeure. Plus particulièrement, lorsque l'on parle simplement « d'accord suspendu » sans plus de précision, cela désigne l'accord de neuvième avec une quarte suspendue, noté « 9sus4 » ou simplement « sus ». == L'harmonie tonale == L'harmonie tonale est un ensemble de règle assez strictes qui s'appliquent dans la musique savante européenne, de la période baroque à la période classique classique ({{pc|xiv}}<sup>e</sup>-{{pc|xviii}}<sup>e</sup> siècle). Certaines règles sont encore largement appliquées dans divers styles musicaux actuels, y compris populaire (rock, rap…), d'autres sont au contraire ignorées (par exemple, un enchaînement de plusieurs accords de même qualité forme un mouvement parallèle, ce qui est proscrit en harmonie tonale). De nos jours, on peut voir ces règles comme des règles « de bon goût », et leur application stricte comme une manière de composer « à la manière de ». Précédemment, nous avons vu la progression des accords. Ci-après, nous abordons aussi la manière dont les notes de l'accord sont réparties entre plusieurs voix, et comment on construit chaque voix. === Concepts fondamentaux === ; Consonance : Les intervalles sont considérés comme « plus ou moins consonants » : :* consonance parfaite : unisson, quinte et octave ; :* consonance mixte (parfaite dans certains contextes, imparfaite dans d'autres) : quarte ; :* consonance imparfaite : tierce et sixte ; :* dissonance : seconde et septième. ; Degrés : Certains degrés sont considérés comme « forts », « meilleurs », ce sont les « notes tonales » : {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (sous-dominante) et {{Times New Roman|V}} (dominante). [[Fichier:Mouvements harmoniques.svg|vignette|upright=0.75|Mouvements harmoniques.]] ; Mouvements : Le mouvement décrit la manière dont les voix évoluent les unes par rapport aux autres : :# Mouvement parallèle : les voix sont séparées par un intervalle constant. :# Mouvement oblique : une voix reste constante, c'est le bourdon ; l'autre monte ou descend. :# Mouvement contraire : une voix descend, l'autre monte. :# Échange de voix : les voix échangent de note ; les mélodies se croisent mais on a toujours le même intervalle harmonique. {{clear}} === Premières règles === ; Règle du plus court chemin : Quand on passe d'un accord à l'autre, la répartition des notes se fait de sorte que chaque voix fait le plus petit mouvement possible. Notamment : si les deux accords ont des notes en commun, alors les voix concernées gardent la même note. : Les deux voix les plus importantes sont la voix aigüe — soprano — et la voix la plus grave — basse. Ces deux voix sont relativement libres : la voix de soprano a la mélodie, la voix de basse fonde l'harmonie. La règle du plus court chemin s'applique surtout aux voix intermédiaires ; si l'on a des mouvements conjoints, ou du moins de petits intervalles — c'est le sens de la règle du plus court chemin —, alors les voix sont plus faciles à interpréter. Cette règle évite également que les voix n'empiètent l'une sur l'autre (voir la règle « éviter le croisement des voix »). ; Éviter les consonances parfaites consécutives :* Lorsque deux voix sont à l'unisson ou à l'octave, elles ne doivent pas garder le même intervalle, l'effet serait trop plat. :* Lorsque deux voix sont à la quarte ou à la quinte, elles ne doivent pas garder le même intervalle, car l'effet est trop dur. : Pour éviter cela, lorsque l'on part d'un intervalle juste, on a intérêt à pratiquer un mouvement contraire aux voix qui ne gardent pas la même note, ou au moins un mouvement direct : les voix vont dans le même sens, mais l'intervalle change. : Notez que même avec le mouvement contraire, on peut avoir des consonances parfaites consécutives, par exemple si une voix fait ''do'' aigu ↗ ''sol'' aigu et l'autre ''sol'' médium ↘ ''do'' grave. : L'interdiction des consonances parfaites consécutives n'a pas été toujours appliquée, le mouvement parallèle strict a d'ailleurs été le premier procédé utilisé dans la musique religieuse au {{pc|x}}<sup>e</sup> siècle. On peut par exemple utiliser des quintes parallèles pour donner un style médiéval au morceau. On peut également utiliser des octaves parallèles sur plusieurs notes afin de créer un effet de renforcement de la mélodie. : Par ailleurs, les consonances parfaites consécutives sont acceptées lorsqu'il s'agit d'une cadence (transition entre deux parties ou bien conclusion du morceau). ; Éviter le croisement des voix : Les voix sont organisées de la plus grave à la plus aigüe. Deux voix n'étant pas à l'unisson, celle qui est plus aigüe ne doit pas devenir la plus grave et ''vice versa''. ; Soigner la partie soprano : Comme c'est celle qu'on entend le mieux, c'est en général celle qui porte la mélodie principale. On lui applique des règles spécifiques : :# Si elle chante la sensible dans un accord de dominante ({{Times New Roman|V}}), alors elle doit monter à la tonique, c'est-à-dire que la note suivante sera la tonique située un demi-ton au dessus. :# Si l'on arrive à une quinte ou une octave entre les parties basse et soprano par un mouvement direct, alors sur la partie soprano, le mouvement doit être conjoint. On doit donc arriver à cette situation par des notes voisines au soprano. ; Préférer certains accords : Les deux degrés les plus importants sont la tonique ({{Times New Roman|I}}) et la dominante ({{Times New Roman|V}}), les accords correspondants ont donc une importance particulière. : À l'inverse, l'accord de sensible ({{Times New Roman|VII}}) n'est pas considéré comme ayant une fonction harmonique forte. On le considère comme un accord de dominante affaibli. En tonalité mineure, on évite également l'accord de médiante ({{Times New Roman|III}}). : Donc on utilise en priorité les accords de : :# {{Times New Roman|I}} et {{Times New Roman|V}}. :# Puis {{Times New Roman|II}}, {{Times New Roman|IV}}, {{Times New Roman|VI}} ; et {{Times New Roman|III}} en mode majeur. :# On évite {{Times New Roman|VII}} ; et {{Times New Roman|III}} en mode mineur. ; Préférer certains enchaînements : Les enchaînements d'accord peuvent être classés par ordre de préférence. Par ordre de préférence décroissante (du « meilleur » au « moins bon ») : :# Meilleurs enchaînements : quarte ascendante ou descendante. Notons que la quarte est le renversement de la quinte, on a donc des enchaînements stables et naturels, mais avec un intervalle plus court qu'un enchaînement de quintes. :# Bons enchaînements : tierce ascendante ou descendante. Les accords consécutifs ont deux notes en commun. :# Enchaînements médiocres : seconde ascendante ou descendante. Les accords sont voisins, mais ils n'ont aucune note en commun. On les utilise de préférence en mouvement ascendant, et on utilise surtout les enchaînements {{Times New Roman|IV}}-{{Times New Roman|V}}, {{Times New Roman|V}}-{{Times New Roman|VI}} et éventuellement {{Times New Roman|I}}-{{Times New Roman|II}}. :# Les autres enchaînements sont à éviter. : On peut atténuer l'effet d'un enchaînement médiocre en plaçant le second accord sur un temps faible ou bien en passant par un accord intermédiaire. [[Fichier:Progression Vplus4 I6.svg|thumb|Résolution d'un accord de triton (quarte sensible) vers l'accord de sixte de la tonique.]] ; La septième descend par mouvement conjoint : Dans un accord de septième de dominante, la septième — qui est donc le degré {{Times New Roman|IV}} — descend par mouvement conjoint — elle est donc suivie du degré {{Times New Roman|III}}. : Corolaire : un accord {{Times New Roman|V}}<sup>+4</sup> se résout par un accord {{Times New Roman|I}}<sup>6</sup> : on a bien un enchaînement {{Times New Roman|V}} → {{Times New Roman|I}}, et la 7{{e}} (degré {{Times New Roman|IV}}), qui est la basse de l'accord {{Times New Roman|V}}<sup>+4</sup>, descend d'un degré pour donner la basse de l'accord {{Times New Roman|I}}<sup>6</sup> (degré {{Times New Roman|III}}). {{clear}} [[Fichier:Progression I64 V7plus I5.svg|thumb|Accord de sixte et de quarte cadentiel.]] ; Un accord de sixte et quarte est un accord de passage : Le second renversement d'un accord parfait est soit une appoggiature, soit un accord de passage, soit un accord de broderie. : S'il s'agit de l'accord de tonique {{Times New Roman|I}}<sup>6</sup><sub>4</sub>, c'est « accord de sixte et quarte de cadence », l'appoggiature de l'accord de dominante de la cadence parfaite. {{clear}} Mais il faut appliquer ces règles avec discernement. Par exemple, la voix la plus aigüe est celle qui s'entend le mieux, c'est donc elle qui porte la mélodie principale. Il est important qu'elle reste la plus aigüe. La voix la plus grave porte l'harmonie, elle pose les accords, il est donc également important qu'elle reste la plus grave. Ceci a deux conséquences : # Ces deux voix extrêmes peuvent avoir des intervalles mélodiques importants et donc déroger à la règle du plus court chemin : la voix aigüe parce que la mélodie prime, la voix de basse parce que la progression d'accords prime. # Les croisements des voix intermédiaires sont moins critiques. Par ailleurs, si l'on applique strictement toutes les règles « meilleurs accords, meilleurs enchaînements », on produit un effet conventionnel, stéréotypé. Il est donc important d'utiliser les solutions « moins bonnes », « médiocres » pour apporter de la variété. Ajoutons que les renversements d'accords permettent d'avoir plus de souplesse : on reste sur le même accord, mais on enrichit la mélodie sur chaque voix. Le ''Bolero'' de Maurice Ravel (1928) brise un certain nombre de ces règles. Par exemple, de la mesure 39 à la mesure 59, la harpe joue des secondes. De la mesure 149 à la mesure 165, les piccolo jouent à la sixte, dans des mouvement strictement parallèle, ce qui donne d'ailleurs une sonorité étrange. À partir de la mesure 239, de nombreux instruments jouent en mouvement parallèles (piccolos, flûtes, hautbois, cor, clarinettes et violons). === Application === [[Fichier:Harmonisation possible de frere jacques exercice.svg|vignette|Exercice : harmoniser ''Frère Jacques''.]] Harmoniser ''Frère Jacques''. Nous considérons un morceau à quatre voix : basse, ténor, alto et soprano. La soprano chante la mélodie de ''Frère Jacques''. L'exercice consiste à proposer l'écriture des trois autres voix en respectant les règles énoncées ci-dessus. Pour simplifier, nous ajoutons les contraintes suivantes : * toutes les voix chantent des blanches ; * nous nous limitons aux accords de quinte (accords de trois sons composés d'une tierce et d'une quinte) sans avoir recours à leurs renversements (accords de sixte, accords de sixte et de quarte). Les notes à gauche de la portée indiquent la tessiture (ou ambitus), l'amplitude que peut chanter la voix. {{clear}} {{boîte déroulante/début|titre=Solution possible}} [[Fichier:Harmonisation possible de frere jacques solution.svg|vignette|Harmonisation possible de ''Frère Jacques'' (solution de l'exercice).]] Il n'y a pas qu'une solution possible. Le premier accord doit contenir un ''do''. Nous sommes manifestement en tonalité de ''do'' majeur, nous proposons de commencer par l'accord parfait de ''do'' majeur, I<sup>5</sup>. Le deuxième accord doit comporter un ''ré''. Si nous utilisons l'accord de quinte de ''ré'', nous allons créer une quinte parallèle. Nous pourrions utiliser un renversement, mais nous nous imposons de chercher un autre accord. Il peut s'agir de l'accord ''si''<sup>5</sup> ''(si-ré-fa)'' ou de l'accord de ''sol''<sup>5</sup> ''(sol-si-ré)''. La dernière solution permet d'utiliser l'accord de dominante qui est un accord important de la tonalité. La règle du plus court chemin imposerait le ''sol'' grave pour la partie de basse, mais cela est proche de la limite du chanteur, nous préférons passer au ''sol'' aigu, plus facile à chanter. Nous vérifions qu'il n'y a pas de quinte parallèle : l'intervalle ascendant ''do-sol'' (basse-alto) devient ''sol-si'' (3<sup>ce</sup>), l'intervalle descendant ''do-sol'' (soprano-alto) devient ''ré-si'' (3<sup>ce</sup>). De la même manière, pour le troisième accord, nous ne pouvons pas passer à un accord de ''la''<sup>5</sup> pour éviter une quinte parallèle. Nous avons le choix entre ''do''<sup>5</sup> ''(do-mi-sol)'' et ''mi''<sup>5</sup> ''(mi-sol-si)''. Nous préférons revenir à l'accord de fondamental, solution très stable (l'enchaînement {{Times New Roman|V}}-{{Times New Roman|I}} formant une cadence parfaite). Pour le quatrième accord, nous pourrions rester sur l'accord parfait de ''do'' mais cela planterait en quelque sorte la fin du morceau puisque l'on resterait sur la cadence parfaite ; or, nous connaissons le morceau et savons qu'il n'est pas fini. Nous choisissons l'accord de ''la''<sup>5</sup> qui est une sixte ascendante ({{Times New Roman|I}}-{{Times New Roman|VI}}). Nos aurions pu répartir les voix différemment. Par exemple : * alto : ''sol''-''si''-''sol''-''do'' ; * ténor : ''mi''-''ré''-''mi''-''mi''. {{boîte déroulante/fin}} [[Fichier:Harmonisation possible de frere jacques.midi|vignette|Fichier son correspondant.]] {{clear}} == Annexe == === Accords en musique classique === Un accord est un ensemble de notes jouées simultanément. Il peut s'agir : * de notes jouées par plusieurs instruments ; * de notes jouées par un même instrument : piano, clavecin, orgue, guitare, harpe (la plupart des instruments à clavier et des instruments à corde). Pour deux notes jouées simultanément, on parle d'intervalle « harmonique » (par opposition à l'intervalle « mélodique » qui concerne les notes jouées successivement). Les notes répétées à différentes octaves ne changent pas la nature de l'accord. La musique classique considère en général des empilements de tierces ; un accord de trois notes sera constitué de deux tierces successives, un accord de quatre notes de trois tierces… Lorsque tous les intervalles sont des intervalles impairs — tierces, quintes, septièmes, neuvièmes, onzièmes, treizièmes… — alors l'accord est dit « à l'état fondamental » (ou encore « primitif » ou « direct »). La note de la plus grave est appelée « fondamentale » de l'accord. Lorsque l'accord comporte un ou des intervalles pairs, l'accord est dit « renversé » ; la note la plus grave est appelée « basse ». De manière plus générale, l'accord est dit à l'état fondamental lorsque la basse est aussi la fondamentale. On a donc un état idéal de l'accord (état canonique) — un empilement strict de tierces — et l'état réel de l'accord — l'empilement des notes réellement jouées, avec d'éventuels redoublements, omissions et inversions ; et seule la basse indique si l'accord est à l'état fondamental ou renversé. Le chiffrage dit de « basse continue » ''({{lang|it|basso continuo}})'' désigne la représentation d'un accord sous la forme d'un ou plusieurs chiffres arabes et éventuellement d'un chiffre romain. ==== Accords de trois notes ==== En musique classique, les seuls accords considérés comme parfaitement consonants, c'est-à-dire sonnant agréablement à l'oreille, sont appelés « accords parfaits ». Si l'on prend une tonalité et un mode donné, alors l'accord construit par superposition es degrés I, III et V de cette gamme porte le nom de la gamme qui l'a généré. [[fichier:Accord do majeur chiffre.svg|vignette|upright=0.5|Accord parfait de ''do'' majeur chiffré.]] Par exemple : * « l'accord parfait de ''do'' majeur » est composé des notes ''do'', ''mi'' et ''sol'' ; * « l'accord parfait de ''la'' mineur » est composé des notes ''la'', ''do'' et ''mi''. Un accord parfait majeur est donc composé, en partant de la fondamentale, d'une tierce majeure et d'une quinte juste. Un accord parfait mineur est composé d'une tierce mineure et d'une quinte juste. L'accord parfait à l'état fondamental est appelé « accord de quinte » et est simplement chiffré « 5 » pour indiquer la quinte. On peut également commencer un accord sur sa deuxième ou sa troisième note, en faisant monter celle(s) qui précède(nt) à l'octave suivante. On parle alors de « renversement d'accord » ou d'accord « renversé ». [[Fichier:Accord do majeur renversements chiffre.svg|vignette|upright=0.75|Accord parfait de ''do'' majeur et ses renversements, chiffrés.]] Par exemple, * le premier renversement de l'accord parfait de ''do'' majeur est :<br /> ''mi'', ''sol'', ''do'' ; * le second renversement de l'accord parfait de do majeur est :<br /> ''sol'', ''do'', ''mi''. Les notes conservent leur nom de « fondamentale », « tierce » et « quinte » malgré le changement d'ordre. La note la plus grave est appelée « basse ». Dans le cas du premier renversement, le deuxième note est la tierce de la basse (la note la plus grave) et la troisième note est la sixte ; le chiffrage en chiffres arabes est donc « 6 » (puisque l'on omet la tierce) et l'accord est appelé « accord de sixte ». Pour le deuxième renversement, les intervalles sont la quarte et la sixte, le chiffrage est donc « 6-4 » et l'accord est appelé « accord de sixte et de quarte ». Dans tous les cas, on chiffre le degré on considérant la fondamentale, par exemple {{Times New Roman|I}} si l'accord est construit sur la tonique de la gamme. Les autres accords de trois notes que l'on rencontre sont : * l'accord de quinte diminuée, constitué d'une tierce mineure et d'une quinte diminuée ; lorsqu'il est construit sur le septième degré d'une gamme, on considère que c'est un accord de septième de dominante sans fondamentale (voir plus bas), le degré est donc indiqué « “{{Times New Roman|V}}” » (cinq entre guillemets) et non « {{Times New Roman|VII}} » ; * l'accord de quinte augmenté : il est composé d'une tierce majeure et qu'une quinte augmentée. Dans le tableau ci-dessous, * « m » désigne un intervalle mineur ; * « M » un intervalle majeur ou le mode majeur ; * « J » un intervalle juste ; * « d » un intervalle diminué ; * « A » un intervalle augmenté ; * « mh » le mode mineur harmonique ; * « ma » le mode mineur ascendant ; * « md » le mode mineur descendant. {| class="wikitable" |+ Accords de trois notes ! scope="col" rowspan="2" | Nom ! scope="col" rowspan="2" | 3<sup>ce</sup> ! scope="col" rowspan="2" | 5<sup>te</sup> ! scope="col" rowspan="2" | État fondamental ! scope="col" rowspan="2" | 1<sup>er</sup> renversement ! scope="col" rowspan="2" | 2<sup>nd</sup> renversement ! scope="col" colspan="4"| Construit sur les degrés |- ! scope="col" | M ! scope="col" | mh ! scope="col" | ma ! scope="col" | md |- | Accord parfait<br /> majeur || M || J | accord de quinte || accord de sixte || accord de<br />sixte et de quarte | {{Times New Roman|I, IV, V}} || {{Times New Roman|V, VI}} || {{Times New Roman|IV, V}} || {{Times New Roman|III, VI, VII}} |- | Accord parfait<br /> mineur || m || J | accord de quinte || accord de sixte || accord de<br />sixte et de quarte | {{Times New Roman|II, III, VI}} || {{Times New Roman|I, IV}} || {{Times New Roman|I, II}} || {{Times New Roman|I, IV, V}} |- | Accord de<br />quinte diminuée || m || d | accord de<br />quinte diminuée || accord de<br />sixte sensible<br />sans fondamentale || accord de triton<br />sans fondamentale | {{Times New Roman|VII (“V”)}} || {{Times New Roman|II, VII (“V”)}} || {{Times New Roman|VI, VII (“V”)}} || {{Times New Roman|II}} |- | Accord de<br />quinte augmentée || M || A | accord de<br />quinte augmentée || accord de sixte<br />et de tierce sensible || accord de sixte et de quarte<br />sur sensible | &nbsp; || {{Times New Roman|III}} || {{Times New Roman|III}} || &nbsp; |} ==== Accords de quatre notes ==== Les accords de quatre notes sont des accord composés de trois tierces superposées. La dernière note étant le septième degré de la gamme, on parle aussi d'accords de septième. Ces accords sont dissonants : ils contiennent un intervalle de septième (soit une octave montante suivie d'une seconde descendante). Ils laissent donc une impression de « tension ». Il existe sept différents types d'accords, ou « espèces ». Citons l'accord de septième de dominante, l'accord de septième mineure et l'accord de septième majeure. ===== L'accord de septième de dominante ===== [[Fichier:Accord 7e dominante do majeur renversements chiffre.svg|vignette|Accord de septième de dominante de ''do'' majeur et ses renversements, chiffrés.]] L'accord de septième de dominante est l'empilement de trois tierces à partir de la dominante de la gamme, c'est-à-dire du {{Times New Roman|V}}<sup>e</sup> degré. Par exemple, l'accord de septième de dominante de ''do'' majeur est l'accord ''sol''-''si''-''ré''-''fa'', et l'accord de septième de dominante de ''la'' mineur est ''mi''-''sol''♯-''si''-''ré''. L'accord de septième de dominante dont la fondamentale est ''do'' (''do''-''mi''-''sol''-''si''♭) appartient à la gamme de ''fa'' majeur. Que le mode soit majeur ou mineur, il est composé d'une tierce majeure, d'une quinte juste et d'une septième mineure (c'est un accord parfait majeur auquel on ajoute une septième mineure). C'est de loin l'accord de septième le plus utilisé ; il apparaît au {{pc|xvii}}<sup>e</sup> en musique classique. Dans son état fondamental, son chiffrage est {{Times New Roman|V 7/+}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}). Le signe plus indique la sensible. Son premier renversement est appelé « accord de quinte diminuée et sixte » et est noté {{Times New Roman|V 6/<s>5</s>}} (ou {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}). Son deuxième renversement est appelé « accord de sixte sensible », puisque la sixte de l'accord est la sensible de la gamme, et est noté {{Times New Roman|V +6}} (ou {{Times New Roman|V<sup>+6</sup>}}). Son troisième renversement est appelé « accord de quarte sensible » et est noté {{Times New Roman|V +4}} (ou {{Times New Roman|V<sup>+4</sup>}}). [[Fichier:Accord 7e dominante sans fondamentale do majeur renversements chiffre.svg|vignette|Accord de septième de dominante sans fondamentale de ''do'' majeur et ses renversements, chiffrés.]] On utilise aussi l'accord de septième de dominante sans fondamentale ; c'est alors un accord de trois notes. Dans son état fondamental, c'est un « accord de quinte diminuée » placé sur le {{Times New Roman|VII}}<sup>e</sup> degré (mais c'est bien un accord construit sur le {{Times New Roman|V}}<sup>e</sup> degré), noté {{Times New Roman|“V” <s>5</s>}} (ou {{Times New Roman|“V”<sup><s>5</s></sup>}}). Notez les guillemets qui indiquent que la fondamentale V est absente. Dans son premier renversement, c'est un « accord de sixte sensible sans fondamentale » noté {{Times New Roman|“V” +6/3}} (ou {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}}). Dans son second renversement, c'est un « accord de triton sans fondamentale » (puisque le premier intervalle est une quarte augmentée qui comporte trois tons) noté {{Times New Roman|“V” 6/+4}} (ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}). Notons qu'un accord de septième de dominante n'a pas toujours la dominante pour fondamentale : tout accord composé d'une tierce majeure, d'une quinte juste et d'une septième mineure est un accord de septième de dominante et est chiffré {{Times New Roman|<sup>7</sup><sub>+</sub>}}, quel que soit le degré sur lequel il est bâti (certaines notes peuvent avoir une altération accidentelle). ===== Les accords de septième d'espèce ===== Les autres accords de septièmes sont dits « d'espèce ». L'accord de septième mineure est l'accord de septième formé sur la fondamentale d'une gamme mineure ''naturelle''. Par exemple, l'accord de septième mineure de ''la'' est ''la''-''do''-''mi''-''sol''. Il est composé d'une tierce mineure, d'une quinte juste et d'une septième mineure (c'est un accord parfait mineur auquel on ajoute une septième mineure). L'accord de septième majeure est l'accord de septième formé sur la fondamentale d'une gamme majeure. Par exemple, L'accord de septième majeure de ''do'' est ''do''-''mi''-''sol''-''si''. Il est composé d'une tierce majeure, d'une quinte juste et d'une septième majeure (c'est un accord parfait majeur auquel on ajoute une septième majeure). ==== Utilisation du chiffrage ==== Le chiffrage est utilisé de deux manières. La première manière, c'est la notation de la basse continue. La basse continue est une technique d'improvisation utilisée dans le baroque pour l'accompagnement d'instruments solistes. Sur la partition, on indique en général la note de basse de l'accord et le chiffrage en chiffres arabes. La seconde manière, c'est pour l'analyse d'une partition. Le fait de chiffrer les accords permet de mieux en comprendre la structure. De manière générale, on peut retenir que : * le chiffrage « 5 » indique un accord parfait, superposition d'une tierce (majeure ou mineure) et d'une quinte juste ; * le chiffrage « 6 » indique le premier renversement d'un accord parfait ; * le chiffrage « 6/4 » indique le second renversement d'un accord parfait ; * chiffrage « 7/+ » indique un accord de septième de dominante ; * le signe « + » indique en général que la note de l'intervalle est la sensible ; * un intervalle barré désigne un intervalle diminué. [[fichier:Accords gamme do majeur la mineur.svg|class=transparent| center | Principaux accords construits sur les gammes de ''do'' majeur et de ''la'' mineur harmonique.]] === Notation « jazz » === En jazz et de manière générale en musique rock et populaire, la base d'un accord est la triade composée d'une tierce (majeure ou mineure) et d'une quinte juste. Pour désigner un accord, on utilise la note fondamentale, éventuellement désigné par une lettre dans le système anglo-saxon (A pour ''la'' etc.), suivi d'une qualité (comme « m », « + »…). Les renversements ne sont pas notés de manière particulière, ils sont notés comme les formes fondamentales. Dans les deux tableaux suivants, la fondamentale est notée X (remplace le C pour un accord de ''do'', le D pour un accord de ''ré''…). La construction des accords est décrite par la suite. [[Fichier:Arbre accords triades 5d5J5A.svg|vignette|upright=1.5|Formation des triades présentée sous forme d'arbre.]] {| class="wikitable" |+ Notation des principales triades |- | ! scope="col" | Tierce<br />mineure (3m) ! scope="col" | Tierce<br />majeure (3M) |- ! scope="row" | Quinte diminuée (5d) | X<sup>o</sup>, Xm<sup>♭5</sup>, X–<sup>♭5</sup> || |- ! scope="row" | Quinte juste (5J) | Xm, X– || X |- ! scope="row" | Quinte augmentée (5A) | || X+, X<sup>♯5</sup> |} [[Fichier:Triades do.svg|class=transparent|center|Triades de do.]] {| class="wikitable" |+ Notation des principaux accords de septième |- | colspan="2" | ! scope="col" | Tierce<br />mineure (3m) ! scope="col" | Tierce<br />majeure (3M) |- ! scope="row" rowspan="2" | Quinte<br />diminuée (5d) ! scope="row" | Septième diminuée (7d) | X<sup>o7</sup> || |- ! scope="row" | Septième mineure (7m) | Xm<sup>7(♭5)</sup>, X–<sup>7(♭5)</sup>, X<sup>Ø</sup> || |- ! scope="row" rowspan="3" | Quinte<br />juste (5J) ! scope="row" | Sixte majeure (6M) | Xm<sup>6</sup> || X<sup>6</sup> |- ! scope="row" | Septième mineure (7m) | Xm<sup>7</sup>, X–<sup>7</sup> || X<sup>7</sup> |- ! scope="row" | Septième majeure (7M) | Xm<sup>maj7</sup>, X–<sup>maj7</sup>, Xm<sup>Δ</sup>, X–<sup>Δ</sup> || X<sup>maj7</sup>, X<sup>Δ</sup> |- ! scope="row" rowspan="2" | Quinte<br />augmentée (5A) ! scope="row" | Septième mineure (7m) | || X+<sup>7</sup> |- ! scope="row" | Septième majeure (7M) | || X+<sup>maj7</sup> |} [[Fichier:Arbre accords septieme.svg|class=transparent|center|Formation des accords de septième présentée sous forme d'arbre.]] &nbsp; &nbsp; [[Fichier:Accords do septieme.svg|class=transparent|center|Accord de do septième.]] On notera que l'intervalle de sixte majeure est l'enharmonique de celui de septième diminuée (6M = 7d). [[File:Principaux accords do.svg|class=transparent|center|Principaux accords de do.]] ==== Triades ==== ; Accords fondés sur une tierce majeure * accord parfait majeur : pas de notation *: p. ex. « ''do'' » ou « C » pour l'accord parfait de ''do'' majeur (''do'' - ''mi'' - ''sol'') ; Accords fondés sur une tierce mineure * accord parfait mineur : « m », « min » ou « – » *: « ''do'' m », « ''do'' – », « Cm », « C– »… pour l'accord parfait de ''do'' mineur (''do'' - ''mi''♭ - ''sol'') ==== Triades modifiées ==== ; Accords fondés sur une tierce majeure * accord augmenté (la quinte est augmentée) : aug, +, ♯5 *: « ''do'' aug », « ''do'' + », « ''do''<sup>♯5</sup> » « Caug », « C+ » ou « C<sup>♯5</sup> » pour l'accord de ''do'' augmenté (''do'' - ''mi'' - ''sol''♯) : L'accord augmenté est un empilement de tierces majeures. Ainsi, un accord augmenté a deux notes communes avec deux autres accords augmentés : C+ (''do'' - ''mi'' - ''sol''♯) a deux notes communes avec A♭+ (''la''♭ - ''do'' - ''mi'') et avec E+ (''mi'' - ''sol''♯ - ''si''♯) ; et on remarque que ces trois accords sont en fait enharmoniques (avec les enharmonies ''la''♭ = ''sol''♯ et ''si''♯ = ''do''). En effet, l'octave comporte six tons (sous la forme de cinq tons et deux demi-tons), et une tierce majeure comporte deux tons, on arrive donc à l'octave en ajoutant une tierce majeure à la dernière note de l'accord. ; Accords fondés sur une tierce mineure * accord diminué (la quinte est diminuée) : dim, o, ♭5 *: « ''do'' dim », « ''do''<sup>o</sup> », « ''do''<sup>♭5</sup> », « Cdim », « C<sup>o</sup> » ou « C<sup>♭5</sup> » pour l'accord de ''do'' diminuné (''do'' - ''mi''♭ - ''sol''♭) : On remarque que la quinte diminuée est l'enharmonique de la quarte augmentée et est l'intervalle appelé « triton » (car composé de trois tons). ; Accords fondés sur une tierce majeure ou mineure * accord suspendu de seconde : la tierce est remplacée par une seconde majeure : sus2 *: « ''do''<sup>sus2</sup> » ou « C<sup>sus2</sup> » pour l'accord de ''do'' majeur suspendu de seconde (''do''-''ré''-''sol'') * accord suspendu de quarte : la tierce est remplacée par une quarte juste : sus4 *: « ''do''<sup>sus4</sup> » ou « C<sup>sus4</sup> » pour l'accord de ''do'' majeur suspendu de quarte (''do''-''fa''-''sol'') ==== Triades appauvries ==== ; Accords fondés sur une tierce majeure ou mineure * accord de puissance : la tierce est omise, l'accord n'est constitué que de la fondamentale et de la quinte juste : 5 *: « ''do''<sup>5</sup> », « C<sup>5</sup> » pour l'accord de puissance de ''do'' (''do'' - ''la'') {{note|Très utilisé dans les musiques rock, hard rock et heavy metal, il est souvent joué renversé (''la'' - ''do'') ou bien avec l'ajout de l'octave (''do'' - ''la'' - ''do'').}} ==== Triades enrichies ==== ; Accords fondés sur une tierce majeure * accord de septième (la 7<sup>e</sup> est mineure) : 7 *: « ''do''<sup>7</sup> », « C<sup>7</sup> » pour l'accord de ''do'' septième, appelé « accord de septième de dominante de ''fa'' majeur » en musique classique (''do'' - ''mi'' - ''sol'' - ''si''♭) * accord de septième majeure : Δ, 7M ou maj7 *: « ''do'' <sup>Δ</sup> », « ''do'' <sup>maj7</sup> », « C<sup>Δ</sup> », « C<sup>7M</sup> »… pour l'accord de ''do'' septième majeure (''do'' - ''mi'' - ''sol'' - ''si'') ; Accords fondés sur une tierce mineure * accord de mineur septième (la tierce et la 7<sup>e</sup> sont mineures) : m7, min7 ou –7 *: « ''do'' m<sup>7</sup> », « ''do'' –<sup>7</sup> », « Cm<sup>7</sup> », « C–<sup>7</sup> »… pour l'accord de ''do'' mineur septième, appelé « accord de septième de dominante de ''fa'' mineur » en musique classique (''do'' - ''mi''♭ - ''sol'' - ''si''♭) * accord mineure septième majeure : m7M, m7maj, mΔ, –7M, –7maj, –Δ *: « ''do'' m<sup>7M</sup> », « ''do'' m<sup>maj7</sup> », « ''do'' –<sup>Δ</sup> », « Cm<sup>7M</sup> », « Cm<sup>maj7</sup> », « C–<sup>Δ</sup> »… pour l'accord de ''do'' mineur septième majeure (''do'' - ''mi''♭ - ''sol'' - ''si'') * accord de septième diminué (la quinte et la septième sont diminuée) : dim 7 ou o7 *: « ''do'' dim<sup>7</sup> », « ''do''<sup>o7</sup> », « Cdim<sup>7</sup> » ou « C<sup>o7</sup> » pour l'accord de ''do'' septième diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si''♭) * accord demi-diminué (seule la quinte est diminuée, la septième est mineure) : Ø ou –7(♭5) *: « ''do''<sup>Ø</sup> », « ''do''<sup>7(♭5)</sup> », « C<sup>Ø</sup> » ou « C<sup>7♭5</sup> » pour l'accord de ''do'' demi-diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si'') === Construction pythagoricienne des accords === Nous avons vu au débuts que lorsque l'on joue deux notes en même temps, leurs vibrations se superposent. Certaines superpositions créent un phénomène de battement désagréable, c'est le cas des secondes. Dans le cas d'une tierce majeure, les fréquences des notes quadruple et quintuple d'une même base : les fréquences s'écrivent 4׃<sub>0</sub> et 5׃<sub>0</sub>. Cette superposition de vibrations est agréable à l'oreille. Nous avons également vu que dans le cas d'une quinte juste, les fréquences sont le double et le triple d'une même base, ou encore le quadruple et sextuple si l'on considère la moitié de cette base. Ainsi, dans un accord parfait majeur, les fréquences des fondamentales des notes sont dans un rapport 4, 5, 6. De même, dans le cas d'un accord parfait mineur, les proportions sont de 1/6, 1/5 et 1/4. {{voir|[[../Caractéristiques_et_notation_des_sons_musicaux#Construction_pythagoricienne_et_gamme_de_sept_tons|Caractéristiques et notation des sons musicaux &gt; Construction pythagoricienne et gamme de sept tons]]}} === Un peu de physique : interférences === Les sons sont des vibrations. Lorsque l'on émet deux sons ou plus simultanément, les vibrations se superposent, on parle en physique « d'interférences ». Le modèle le plus simple pour décrire une vibration est la [[w:fr:Fonction sinus|fonction sinus]] : la pression de l'air P varie en fonction du temps ''t'' (en secondes, s), et l'on a pour un son « pur » : : P(''t'') ≈ sin(2π⋅ƒ⋅''t'') où ƒ est la fréquence (en hertz, Hz) du son. Si l'on émet deux sons de fréquence respective ƒ<sub>1</sub> et ƒ<sub>2</sub>, alors la pression vaut : : P(''t'') ≈ sin(2π⋅ƒ<sub>1</sub>⋅''t'') + sin(2π⋅ƒ<sub>2</sub>⋅''t''). Nous avons ici une [[w:fr:Identité trigonométrique#Transformation_de_sommes_en_produits,_ou_antilinéarisation|identité trigonométrique]] dite « antilinéarisation » : : <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{f_1 + f_2}{2}t \right ) \cdot \sin \left ( 2\pi \frac{f_1 - f_2}{2}t \right ).</math> On peut étudier simplement deux situations simples. [[Fichier:Battements interferentiels.png|vignette|Deux sons de fréquences proches créent des battements : la superposition d'une fréquence et d'une enveloppe.]] La première, c'est quand les fréquences ƒ<sub>1</sub> et ƒ<sub>2</sub> sont très proches. Alors, la moyenne (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2 est très proche de ƒ<sub>1</sub> et ƒ<sub>2</sub> ; et la demie différence (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2 est très proche de zéro. On a donc une enveloppe de fréquence très faible, (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2, dans laquelle s'inscrit un son de fréquence moyenne, (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2. C'est cette enveloppe de fréquence très faible qui crée les battements, désagréables à l'oreille. Sur l'image ci-contre, le premier trait rouge montre un instant où les vibrations sont opposées ; elles s'annulent, le son s'éteint. Le second trait rouge montre un instant où les vibrations sont en phase : elle s'ajoutent, le son est au plus fort. {{clear}} La seconde, c'est lorsque les deux fréquences sont des multiples entiers d'une même fréquence fondamentale ƒ<sub>0</sub> : ƒ<sub>1</sub> = ''n''<sub>1</sub>⋅ƒ<sub>0</sub> et ƒ<sub>0</sub> = ''n''<sub>0</sub>⋅ƒ<sub>0</sub>. On a alors : : <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{n_1 + n_2}{2}f_0 \cdot t \right ) \cdot \sin \left ( 2\pi \frac{n_1 - n_2}{2}f_0 \cdot t \right ).</math> On multiplie donc deux fonctions qui ont des fréquences multiples de ƒ<sub>0</sub>. La différence minimale entre ''n''<sub>1</sub> et ''n''<sub>2</sub> vaut 1 ; on a donc une enveloppe dont la fréquence est au minimum la moitié de ƒ<sub>0</sub>, c'est-à-dire un son une octave en dessous de ƒ<sub>0</sub>. Donc, cette enveloppe ne crée pas d'effet de battement, ou plutôt, le battement est trop rapide pour être perçu comme tel. Dans cette enveloppe, on a une fonction sinus dont la fréquence est également un multiple de ƒ<sub>0</sub> ; l'enveloppe et la fonction qui y est inscrite ont donc de nombreux « points communs », d'où l'effet harmonieux. === Le tonnetz === [[File:Speculum musicae.png|thumb|right|225px|Euler, ''De harmoniæ veris principiis'', 1774, p. 350.]] En allemand, le terme ''Tonnetz'' (se prononce « tône-netz ») signifie « réseau tonal ». C'est une représentation graphique des notes qui a été imaginée par [[w:Leonhard Euler|Leonhard Euler]] en 1739. Cette représentation graphique peut aider à la mémorisation de certains concepts de l'harmonie. Cependant, son application est très limitée : elle ne concerne que l'intonation juste d'une part, et que les accords parfait des tonalités majeures et mineures naturelles d'autre part. La représentation contenant les douze notes de la musique savante occidentale, on peut bien sûr représenter d'autres objets, comme les accords de septième ou les accords diminués, mais la représentation graphique est alors compliquée et perd son intérêt pédagogique. On part d'une note, par exemple le ''do''. Si on progresse vers la droite, on monte d'une quinte juste, donc ''sol'' ; vers la gauche, on descend d'une quinte juste, donc ''fa''. Si on va vers le bas, on monte d'une tierce majeure, donc ''mi'' ; si on va vers le haut, on descend d'une tierce majeure, donc ''la''♭ ou ''sol''♯ fa — do — sol — ré | | | | la — mi — si — fa♯ | | | | do♯ — sol♯ — ré♯ — si♭ La figure forme donc un filet, un réseau. On voit que ce réseau « boucle » : si on descend depuis le ''do''♯, on monte d'une tierce majeure, on obtient un ''mi''♯ qui est l'enharmonique du ''fa'' qui est en haut de la colonne. Si on va vers la droite à partir du ''ré'', on obtient le ''la'' qui est au début de la ligne suivante. Si on ajoute des diagonales allant vers la droite et le haut « / », on met en évidence des tierces mineures : ''la'' - ''do'', ''mi'' - ''sol'', ''si'' - ''ré'', ''do''♯ - ''mi''… fa — do — sol — ré | / | / | / | la — mi — si — fa♯ | / | / | / | do♯ — sol♯ — ré♯ — si♭ Donc les liens représentent : * | : tierce majeure ; * — : quinte juste ; * / : tierce mineure. [[Fichier:Tonnetz carre accords fr.svg|thumb|Tonnetz avec les accords parfaits. Les notes sont en notation italienne et les accords en notation jazz.]] On met ainsi en évidence des triangles dont un côté est une quinte juste, un côté une tierce majeure et un côté une tierce mineure ; c'est-à-dire que les notes aux sommets du triangle forment un accord parfait majeur (par exemple ''do'' - ''mi'' - ''sol'') : <div style="font-family:courier; background-color:#fafafa"> fa&nbsp;&nbsp;—&nbsp;'''do&nbsp;&nbsp;&nbsp;—&nbsp;sol'''&nbsp;—&nbsp;ré<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;'''|&nbsp;&nbsp;&nbsp;/'''&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;|<br /> la&nbsp;&nbsp;—&nbsp;'''mi'''&nbsp;&nbsp;&nbsp;—&nbsp;si&nbsp;&nbsp;—&nbsp;fa♯<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;/&nbsp;&nbsp;|<br /> do♯&nbsp;—&nbsp;sol♯&nbsp;—&nbsp;ré♯&nbsp;—&nbsp;si♭ </div> ou un accord parfait mineur (''la'' - ''do'' - ''mi''). <div style="font-family:courier; background-color:#fafafa"> fa&nbsp;&nbsp;—&nbsp;'''do'''&nbsp;&nbsp;&nbsp;—&nbsp;sol&nbsp;—&nbsp;ré<br /> |&nbsp;&nbsp;'''/&nbsp;&nbsp;|'''&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;|<br /> '''la&nbsp;&nbsp;—&nbsp;mi'''&nbsp;&nbsp;&nbsp;—&nbsp;si&nbsp;&nbsp;—&nbsp;fa♯<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;/&nbsp;&nbsp;|<br /> do♯&nbsp;—&nbsp;sol♯&nbsp;—&nbsp;ré♯&nbsp;—&nbsp;si♭ </div> Un triangle représente donc un accord, et un sommet représente une note. Si on passe d'un triangle à un triangle voisin, alors on passe d'un accord à un autre accord, les deux accords ayant deux notes en commun. Ceci illustre la notion de « plus court chemin » en harmonie : si on passe d'un accord à un autre en gardant un côté commun, alors on a un mouvement conjoint sur une seule des trois voix. Par rapport à l'harmonie fonctionnelle : les accords sont contigus à leur fonction, par exemple en ''do'' majeur : * fonction de tonique ({{Times New Roman|I}}) : C, A– et E– sont contigus ; * fonction de sous-dominante ({{Times New Roman|IV}}) : F et D– sont contigus ; * fonction de dominante ({{Times New Roman|V}}) : G et B<sup>o</sup> sont contigus. On notera que les triangles d'un schéma ''tonnetz'' ne représentent que des accords parfaits. Pour représenter un accord de quinte diminuée (''si'' - ''ré'' - ''fa'') ou les accords de septième, en particulier l'accord de septième de dominante, il faut étendre le ''tonnetz'' et l'on obtient des figures différentes. Par ailleurs, il est adapté à ce que l'on appelle « l'intonation juste », puisque tous les intervalles sont idéaux. [[Fichier:Tonnetz carre accords etendu fr.svg|vignette|Tonnetz étendu.]] [[Fichier:Tonnetz carre do majeur accords fr.svg|vignette|Tonnetz de la tonalité de ''do'' majeur. La représentation de l'accord de quinte diminuée sur ''si'' (B<sup>o</sup>) est une ligne et non un triangle.]] [[Fichier:Tonnetz carre do mineur accords fr.svg|vignette|Tonnetz des tonalités de ''do'' mineur naturel (haut) et ''do'' mineur harmonique (bas).]] Si l'on étend un peu le réseau : ré♭ — la♭ — mi♭ — si♭ — fa | / | / | / | / | fa — do — sol — ré — la | / | / | / | / | la — mi — si — fa♯ — do♯ | / | / | / | / | do♯ — sol♯ — ré♯ — la♯ — mi♯ | / | / | / | / | mi♯ — do — sol — ré — la on peut donc trouver des chemins permettant de représenter les accords de septième de dominante (par exemple en ''do'' majeur, G<sup>7</sup>) fa / sol — ré | / si et des accords de quinte diminuée (en ''do'' majeur : B<sup>o</sup>) fa / ré / si Une gamme majeure ou mineure naturelle peut se représenter par un trapèze rectangle : ''do'' majeur fa — do — sol — ré | / la — mi — si et ''do'' mineur la♭ — mi♭ — si♭ / | fa — do — sol — ré En revanche, la représentation d'une tonalité nécessite d'étendre le réseau afin de pouvoir faire figurer tous les accords, deux notes sont représentées deux fois. La représentation des tonalités mineures harmoniques prend une forme biscornue, ce qui nuit à l'intérêt pédagogique de la représentation. [[Fichier:Neo-Riemannian Tonnetz.svg|vignette|upright=2|Tonnetz avec des triangles équilatéraux.]] On peut réorganiser le schéma en décalant les lignes, afin d'avoir des triangles équilatéraux. Sur la figure ci-contre (en notation anglo-saxonne) : * si on monte en allant vers la droite « / », on a une tierce mineure ; * si on descend en allant vers la droite « \ », on a une tierce majeure ; * les liens horizontaux « — » représentent toujours des quintes justes * les triangles pointe en haut sont des accords parfaits mineurs ; * les triangles pointe en bas sont des accords parfaits majeurs. On a alors les accords de septième de dominante F / G — D \ / B et de quinte diminuée F / D / B les tonalités majeures F — C — G — D \ / A — E — B et les tonalités mineures naturelles A♭ — E♭ — B♭ / \ F — C — G — D == Notes et références == {{références}} == Voir aussi == === Bibliographie === * {{ouvrage |prénom1=Francesco |nom1=Gasparini |titre=L'Armonico Pratico al Cimbalo |éditeur=Giusseppe Antonio Silvani |lieu=Bologne |année=1772 |lang=it |url=https://imslp.org/wiki/L'Armonico_Pratico_al_Cimbalo_(Gasparini%2C_Francesco)}} === Liens externes === {{wikipédia|Consonance (harmonie tonale)}} {{wikipédia|Disposition de l'accord}} {{wikisource|Petit Manuel d’harmonie}} * {{lien web | url = https://www.apprendrelesolfege.com/chiffrage-d-accords | titre = Chiffrage d'accords (classique) | site = Apprendrelesolfege.com | consulté le = 2020-12-03 }} * {{lien web | url = https://www.coursd-harmonie.fr/introduction/introduction2_le_chiffrage_d_accords.php | titre = Introduction II : Le chiffrage d'accords | site = Cours d'harmonie.fr | consulté le = 2021-12-14 }} * {{lien web | url=https://www.coursd-harmonie.fr/ | titre = Cours d'harmonie en ligne | auteur = Jean-Baptiste Voinet | site=coursd-harmonie.fr | consulté le = 2021-12-20 }} * {{lien web | url=http://e-harmonie.e-monsite.com/ | titre = Cours d'harmonie classique en ligne | auteur = Olivier Miquel | site=e-harmonie | consulté le = 2021-12-24 }} * {{lien web | url=https://fr.audiofanzine.com/theorie-musicale/editorial/dossiers/les-gammes-et-les-modes.html | titre = Les bases de l’harmonie | site = AudioFanzine | date = 2013-07-23 | consulté le = 2024-01-12 }} * {{lien web | url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/l-accord-de-tristan-dechiffre-et-desacralise-2481605 | titre = L’Accord de Tristan déchiffré et désacralisé ! | site = France Musique | date = 2025-10-22 | consulté le = 2025-10-23 }} * {{lien web | url=https://www.youtube.com/watch?v=X62SAnyfQYw | titre = Qu'est-ce que la basse continue dans la musique baroque ? | auteur = Clément Rochefort | site = YouTube/France Musique | date = 2022-05-02 | consulté le = 2026-08-04 }} ---- ''[[../Mélodie|Mélodie]]'' &lt; [[../|↑]] &gt; ''[[../Représentation musicale|Représentation musicale]]'' [[Catégorie:Formation musicale (livre)|Harmonie]] h7lhylbkjnb899ohcexaf6rayzyzrey 773911 773910 2026-10-02T08:56:23Z Cdang 1202 /* Notation des accords de trois notes */ lecture à voix haute des accords chiffrés 773911 wikitext text/x-wiki {{Bases de solfège}} <span style="font-size:25px;">6. Harmonie</span> L'harmonie désigne les notes jouées en même temps, soit plusieurs instruments jouant chacun une note, soit un instrument jouant un accord (instrument dit polyphonique). == Première approche == L'exemple le plus simple d'harmonie est sans doute la chanson en canon : c'est un chant polyphonique, c'est-à-dire à plusieurs voix, chaque voix chantant la même chose en décalé. Prenons par exemple ''Vent frais, vent du matin'' (la version originale est ''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609) : [[Fichier:Vent frais vent du matin.svg|class=transparent|center|Partition de ''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]] [[Fichier:Vent frais vent du matin.midi|vignette|''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]] nous voyons que les voix se superposent de manière « harmonieuse ». Les notes de chaque voix se correspondent point par point (avec un retard), c'est donc un type d'harmonie polyphonique appelé « contrepoint ». Considérons la première note de la mesure 6 pour chaque voix. Nous avons la superposition des notes ''ré''-''fa''-''la'' (du grave vers l'aigu) ; la superposition de notes jouées ou chantées ensembles s'appelle un accord. Cet accord ''ré''-''fa''-''la'' porte le nom « d'accord parfait de ''ré'' mineur » : * « ''ré'' » car la note fondamentale est un ''ré'' ; * « parfait » car il est l'association d'une tierce, ''ré''-''fa'', et d'une quinte juste, ''ré''-''la'' ; * « mineur » car le premier intervalle, ''ré''-''fa'', est une tierce mineure. Considérons maintenant un chant accompagné au piano. Le piano peut jouer plusieurs notes en même temps, il peut jouer des accords. [[Fichier:Au clair de le lune chant et piano.svg|class=transparent|center|Deux premières mesure d’Au clair de la lune.]] [[Fichier:Au clair de le lune chant et piano.midi|vignette|Deux premières mesure d’Au clair de la lune.]] L'accord, les notes à jouer simultanément, sont écrites « en colonne ». Lorsqu'on les énonce, on les lit de bas en haut mais le pianiste les joue en pressant les touches du clavier en même temps, de manière « plaquée ». Le premier accord est composé des notes ''do''-''mi''-''sol'' ; il est appelé « accord parfait de ''do'' majeur » car la note fondamentale est ''do'', qu'il est l'association d'une tierce et d'une quinte juste et que le premier intervalle, ''do''-''mi'', est une tierce majeure. Si l'on lit toutes les notes, de la plus grave à la plus aiguë, nous avons : ''do'' (à la clef de ''fa'', main gauche du piano) - ''do'' - ''mi'' - ''sol'' (main droite du piano) - ''do'' (chant) ; nous voyons que la note ''do'' est jouée plusieurs fois, cela ne change pas la qualité de l'accord. == Consonance et dissonance == [[Fichier:Seconde majeure septieme mineure.svg|vignette|Seconde majeure et son renversement, le septième mineure.]] Les notions de consonance et de dissonance sont culturelles et changent selon l'époque. Nous pouvons néanmoins noter que : * l'accord de seconde, et son renversement la septième, créent des battements, les notes « frottent », c'est un intervalle harmonique dissonant ; mais dans le cas de la septième, comme les notes sont éloignées, le frottement est moins perceptible ; * les accords d'unisson, de tierce, quarte et quinte sonnent agréablement à l'oreille, ils sont consonants. Dans la musique savante européenne, au début du Moyen-Âge, seuls les accords d'unisson, de quarte et de quinte étaient considérés comme consonants, d'où leur qualification de « juste ». La tierce, et son renversement la sixte, étaient perçues comme dissonantes ; à partir de la Renaissance, ces deux intervalles sont considérés comme consonants. L'harmonie joue avec les consonances et les dissonances. Dans un premier temps, les harmonies dissonantes sont utilisées pour créer des tensions qui sont ensuite résolues, on utilise des successions « consonant-dissonant-consonant ». À force d'entendre des intervalles considérés comme dissonants, l'oreille s'habitue et certains finissent par être considérés comme consonants ; c'est ce qui est arrivé à la tierce et à la sixte à la fin du Moyen Âge avec le contrepoint, puis à la septième plus tardivement. Il faut ici aborder la notion d'harmonique des notes. [[File:Harmoniques de do.svg|thumb|Les six premières harmoniques de ''do''.]] Lorsque l'on joue une note, on entend d'autres notes plus aigües et plus faibles ; la note jouée est appelée la « fondamentale » et les notes plus aigües et plus faibles sont les « harmoniques ». C'est cette accumulation d'harmoniques qui donne la couleur au son, son timbre, qui fait qu'un piano ne sonne pas comme un violon. Par exemple, si l'on joue un ''do''<sup>1</sup><ref>Pour la notation des octaves, voir ''[[../Représentation_musicale#Désignation_des_octaves|Représentation musicale &gt; Désignation des octaves]]''.</ref> (fondamentale), on entend le ''do''<sup>2</sup> (une octave plus aigu), puis un ''sol''<sup>2</sup>, puis encore un ''do''<sup>3</sup> plus aigu, puis un ''mi''<sup>3</sup>, puis encore un ''sol''<sup>3</sup>, puis un ''si''♭<sup>3</sup>… Ainsi, puisque lorsque l'on joue un ''do'' on entend aussi un ''sol'' très léger, alors jouer un ''do'' et un ''sol'' simultanément n'est pas choquant. De même pour ''do'' et ''mi''. De là vient la notion de consonance. Le statut du ''si''♭ est plus ambigu. Il fait partie des harmoniques qui sonnent naturellement, mais il forme une seconde descendante avec le ''do'', intervalle dissonant. Par ailleurs, on remarque que le ''si''♭ ne fait pas partie de la gamme de ''do'' majeur, contrairement au ''sol'' et au ''mi''. Pour le jeu sur les dissonances, on peut écouter par exemple la ''Toccata'' en ''ré'' mineur, op. 11 de Sergueï Prokofiev (1912). : {{lien web |url=https://www.youtube.com/watch?v=AVpnr8dI_50 |titre=Yuja Wang Prokofiev Toccata |site=YouTube |date=2019-02-26 |consulté le=2021-12-19}} == Contrepoint == Dans le chant grégorien, la notion d'accord n'existe pas. L'harmonie provient de la superposition de plusieurs mélodies, notamment dans ce que l'on appelle le « contrepoint ». Le terme provient du latin ''« punctum contra punctum »'', littéralement « point par point », et désigne le fait que les notes de chaque voix se correspondent. L'exemple le plus connu de contrepoint est le canon, comme ''Frère Jacques'' ou bien ''Vent frais, vent du matin'' présenté ci-dessus : chaque note d'un couplet correspond à une note du couplet précédent. Certains morceaux sont bâtis sur une écriture « en miroir » : l'ordre des notes est inversé entre les deux voix, ou bien les intervalles sont inversés (« mouvement contraire » : une tierce montante sur une voix correspond à une tierce descendante sur l'autre). On peut également citer le « mouvement oblique » (une des voix, le bourdon, chante toujours la même note) et le mouvement parallèle (les deux voix chantent le même air mais transposé, l'une est plus aiguë que l'autre). Nous reproduisons ci-dessous le début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel. [[Fichier:Haendel Sonate en trio re mineur debut canon.svg | vignette | center | upright=2 | Début du second ''Allergo'' de la sonate en trio en ''ré'' mineur de Haendel.]] [[Fichier:Haendel Sonate en trio re mineur debut.midi | vignette | Début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.]] Nous avons mis en évidence la construction en canon avec des encadrés de couleur : sur les quatre premières mesures, nous voyons trois motifs repris alternativement par une voix et par l'autre. Ce type de procédé est très courant dans la musique baroque. Les procédés du contrepoint s'appliquent également à la danse : * unisson : les danseurs et danseuses font les mêmes gestes en même temps ; * répétition : le fait de répéter une série de gestes, une « phrase dansante » ; * canon : les gestes sont faits avec un décalage régulier d'un danseur ou d'une danseuse à l'autre ; * cascade : forme de canon dans laquelle le décalage est très petit ; * contraste : deux danseur·euses, ou deux groupes, ont des gestuelles très différentes ; * accumulation : la gestuelle se complexifie par l'ajout d'éléments au fur et à mesure ; ou bien le nombre de danseur·euses augmente ; * dialogue : les gestes de danseur·euses ou de groupes se répondent ; * contre-point : la gestuelle d'un ou une danseuse se superpose à la gestuelle d'un groupe ; * lâcher-rattraper : les danseurs et danseuses alternent danse à l'unisson et gestuelles indépendantes. : {{lien web | url=https://www.youtube.com/watch?v=wgblAOzedFc | titre=Les procédés de composition en danse | auteur= Doisneau Sport TV | site=YouTube | date=2020-03-16 | consulté le=2021-01-21 }} {{...}} == Ostinato == L'ostinato, mot italien signifiant « obstiné », c'est le fait de jouer en boucle un passage. L'ostinato peut ainsi former une structure autour de laquelle peut se développer le morceau. Lorsque l'ostinato est joué par l'instrument de basse (basson, violoncelle, viole de gambe basse, flûte à bec basse, main gauche du piano…), on parle de basse obstinée. Les grilles d'accord en jazz sont des ostinatos (ou ostinati) joués par la section rythmique. Exemples : * {{lien web |url=https://www.youtube.com/watch?v=QZpGe5rNJkI&t=41s |titre=Epic Patty Cake Song (I'll Think Of You) |auteur= Kurt Hugo Schneider |site=YouTube |date=2014-08-12 |consulté le=2026-01-24}} : un ostinato de percussions corporelles ; * {{lien web |url=https://www.youtube.com/watch?v=Kv-37-nXC48 |titre=Pow Wow ''Le lion est mort ce soir'' Les Victoires de la Musique 1993 |auteur=Solomon Linda |date=2015-02-02 |consulté le=2026-01-24}} : dans ce chant ''a capella'' (sans accompagnement d'instrument), les voix de basse forment une basse obstinée, sur les syllabes « oëap » durant le couplet et « awimbowé » sur le refrain ; * {{lien web |url=https://www.youtube.com/watch?v=JvNQLJ1_HQ0 |titre=Pachelbel Canon in D Major - the original and best version. |auteur=Voices of Music |site=YouTube |date=2008-09-02 |consulté le=2026-01-24}} : le ''Canon de Pachelbel de ''ré'' mineur'' (''Canon per tre Violini e Basso'', Johann Pachelbel, vers 1680) est construit sur une basse obstinée de huit mesures. == Les accords en général == Initialement, on a des chants polyphoniques, des voix qui chantent chacune une mélodie, les mélodies se mêlant. On remarque que certaines superpositions de notes sonnent de manière plus ou moins agréables, consonantes ou dissonantes. On en vient alors à associer ces notes, c'est-à-dire à considérer dès le départ la superposition de ces notes et non pas la rencontre de ces notes au gré des mélodies. Ces groupes de notes superposées forment les accords. En Europe, cette notion apparaît vers le {{pc|xiv}}<sup>e</sup> siècle avec notamment la ''[[wikipedia:fr:Messe de Notre Dame|Messe de Notre Dame]]'' de Guillaume de Machaut (vers 1360-1365). À l'origine, dans les chants polyphoniques, les voix sont d'égale importance ; pour un chant à trois voix, les voix sont appelées, de la plus grave à la plus aigüe, « basse », « taille » (ou ténor) et « dessus » (ou soprano). Puis, au cours du temps, une des voix devient plus importante, le « cantus firmus » ; initialement, il s'agit de la voix intermédiaire, la taille (ténor), puis c'est la voix la plus aigüe, le dessus (soprano). À la Renaissance, au {{pc|xvi}}{{e}} siècle, apparaît la notion de « monodie accompagnée » : la voix principale, le cantus firmus, devient la voix mélodique, la monodie, et les autres voix servent à l'accompagner. À l'époque baroque, au {{pc|xvii}}{{e}} siècle, apparaît la notion d'accord, notes sonnant simultanément, avec une notation simplifiée, la « basse chiffrée » : a lieu d'écrire toutes les notes de l'accord, on écrit la basse et des chiffres indiquent quelles sont les autres notes à jouer. La notion « d'accord parfait » est consacrée par [[wikipedia:fr:Jean-Philippe Rameau|Jean-Philippe Rameau]] dans son ''Traité de l'harmonie réduite à ses principes naturels'', publié en 1722. === Qu'est-ce qu'un accord ? === Un accord est un ensemble d'au minimum trois notes jouées en même temps. « Jouées » signifie qu'il faut qu'à un moment donné, elles sonnent en même temps, mais le début ou la fin des notes peut être à des instants différents. Considérons que l'on joue les notes ''do'', ''mi'' et ''sol'' en même temps. Cet accord s'appelle « accord de ''do'' majeur ». En musique classique, on lui adjoint l'adjectif « parfait » : « accord parfait de ''do'' majeur ». Nous représentons ci-dessous trois manière de faire l'accord : avec trois instruments jouant chacun une note : [[Fichier:Do majeur trois portees.svg|class=transparent|center|Accord de ''do'' majeur avec trois instruments différents.]] Avec un seul instrument jouant simultanément les trois notes : [[Fichier:Chord C.svg|class=transparent|center|Accord de ''do'' majeur joué par un seul instrument.]] L'accord tel qu'il est joué habituellement par une guitare d'accompagnement : [[Fichier:Do majeur guitare.svg|class=transparent|center|Accord de ''do'' majeur à la guitare.]] Pour ce dernier, nous représentons le diagramme indiquant la position des doigts sur le manche au dessus de la portée et la tablature en dessous. Ici, c'est au total six notes qui sont jouées : ''mi'' grave, ''do'' médium, ''mi'' médium, ''sol'' médium, ''do'' aigu, ''mi'' aigu. Mais il s'agit bien des trois notes ''do'', ''mi'' et ''sol'' jouées à des octaves différentes. Nous remarquons également que la note de basse (la note la plus grave), ''mi'', est différente de la note fondamentale (celle qui donne le nom à l'accord), ''do'' ; l'accord est dit « renversé » (voir plus loin). === Comment joue-t-on un accord ? === Les notes ne sont pas forcément jouées en même temps ; elles peuvent être « égrainées », jouée successivement, ce que l'on appelle un arpège. La partition ci-dessous montre six manières différentes de jouer un accord de ''la'' mineur à la guitare, plaqué puis arpégé. [[Fichier:La mineur differentes executions.svg|class=transparent|center|Différentes exécution de l'accord de do majeur à la guitare.]] [[Fichier:La mineur differentes executions midi.midi|vignette|Différentes exécution de l'accord de la mineur à la guitare.]] Vous pouvez écouter l'exécution de cette partition avec le lecteur ci-contre. Seuls les instruments polyphoniques peuvent jouer les accords plaqués : instruments à clavier (clavecin, orgue, piano, accordéon), les instruments à plusieurs cordes pincées (harpe, guitare ; violon, alto, violoncelle et contrebasse joués en pizzicati). Les instruments à corde frottés de la famille du violon peuvent jouer des notes par deux à l'archet mais pas plus du fait de la forme bombée du chevalet ; cependant, un mouvement rapide permet de jouer les quatre cordes de manière très rapprochée. Les instruments à percussion de type xylophone ou le tympanon permettent de jouer jusqu'à quatre notes simultanément en tenant deux baguettes (mailloches, maillets) par main. Tous les instruments peuvent jouer des arpèges même si, dans le cas des instruments monodiques, les notes ne continuent pas à sonner lorsque l'on passe à la note suivante. L'arpège peut être joué par l'instrument de basse (basson, violoncelle, contrebasse, guitare basse, pédalier de l'orgue…), notamment dans le cas d'une basse continue ou d'une ''{{lang|en|walking bass}}'' (« basse marchante » : la basse joue des noires, donnant ainsi l'impression qu'elle marche). En jazz, et spécifiquement au piano, on a recours au ''{{lang|en|voicing}}'' : on choisit la manière dont on organise les notes pour donner une couleur spécifique, ou bien pour créer une mélodie en enchaînant les accords. Il est fréquent de ne pas jouer toutes les notes : si on n'en garde que deux, ce sont la tierce et la septième, car ce sont celles qui caractérisent l'accord (selon que la tierce est mineure ou majeure, que la septième est majeure ou mineure), et la fondamentale est en général jouée par la contrebasse ou guitare basse. {{clear}} === Classes d'accord === [[Fichier:Intervalles harmoniques accords classes.svg|vignette|upright=1.5|Intervalles harmoniques dans les accords classés de trois, quatre et cinq notes.]] Un accord composé d'empilement de tierces est appelé « accord classé ». En musique tonale, c'est-à-dire la musique fondée sur les gammes majeures ou mineures (cas majoritaire en musique classique), on distingue trois classes d'accords : * les accords de trois notes, ou triades, ou accords de quinte ; * les accords de quatre notes, ou accords de septième ; * les accords de cinq notes, ou accords de neuvième. En empilant des tierces, si l'on part de la note fondamentale, on a donc de intervalles de tierce, quinte, septième et neuvième. En musique tonale, les accords avec d'autres intervalles (hors renversement, voir ci-après), typiquement seconde, quarte ou sixte, sont considérés comme des transitions entre deux accords classés. Ils sont appelés, selon leur utilisation, « accords à retard » (en anglais : ''{{lang|en|suspended chord}}'', accord suspendu) ou « appoggiature » (note « appuyée », étrangère à l'harmonie). Voir aussi plus loin la notion de note étrangère. === Renversements d'accords === [[File:Accord do majeur renversements.svg|thumb|Accord parfait de do majeur et ses renversements.]] [[Fichier:Progression dominante renverse parfait do majeur.svg|vignette|upright=0.6|Progression accord de dominante renversé → accord parfait en ''do'' majeur.]] Un accord classé est donc un empilement de tierces. Si l'on change l'ordre des notes, on a toujours le même accord mais il est fait avec d'autres intervalles harmoniques. Par exemple, l'accord parfait de ''do'' majeur dans son état fondamental, c'est-à-dire non renversé, s'écrit ''do'' - ''mi'' - ''sol''. Sa note fondamentale, ''do'', est aussi se note de basse. Si maintenant on prend le ''do'' de l'octave supérieure, l'accord devient ''mi - sol - do'' ; c'est l'empilement d'une tierce ''(mi - sol)'' et d'une quarte ''(sol - do)'', soit la superposition d'une tierce ''(mi - sol)'' et d'une sixième ''(mi - do)''. C'est le premier renversement de l'accord parfait de ''do'' majeur ; la fondamentale est toujours ''do'' mais la basse est ''mi''. Le second renversement est ''sol - do - mi''. L'utilisation de renversement peut faciliter l'exécution de la progression d'accord. Par exemple, en tonalité ''do'' majeur, si l'on veut passer de l'accord de dominante ''sol - si - ré'' à l'accord parfait ''do - mi - sol'', alors on peut utiliser le second renversement de l'accord de dominante : ''ré - sol - si'' → ''do - mi - sol''. Ainsi, la basse descend juste d'un ton ''(ré → do)'' et sur un piano, la main reste globalement dans la même position. Le renversement d'un accord permet également de respecter certaines règles de l'harmonie classique, notamment éviter que des voix se suivent strictement (« mouvement parallèle »), ce qui aurait un effet de platitude. De manière générale, la notion de renversement permet deux choses : * d'enrichir l'œuvre : pour créer une harmonie donnée (c'est-à-dire des sons sonnant bien ensemble), nous avons plus de souplesse, nous pouvons organiser ces notes comme nous le voulons selon les voix ; * de simplifier l'analyse : quelle que soit la manière dont sont organisées les notes, cela nous ramène à un même accord. {{citation bloc|Or il, y a plusieurs manières de jouer un accord, selon que l'on aborde par la première note qui le constitue, ''do mi sol'', la deuxième, ''mi sol do'', ou la troisième note, ''sol do mi''. Ce sont les renversements, [que Rameau] va classer en différentes combinaisons d'une seule matrice. Faisant cela, Rameau divise le nombre d'accords [de septième] par quatre. Il simplifie, il structure […].|{{ouvrage|prénom1=André |nom1=Manoukian |titre=Sur les routes de la musique |éditeur=Harper Collins |année=2021 |passage=54 |isbn=979-1-03391201-9}} }} {{clear}} [[File:Plusieurs realisation 1er renversement doM.svg|thumb|Plusieurs réalisation du premier renversement de l'accord de ''do'' majeur.]] Notez que # Les notes peuvent être répétées plusieurs fois, à différentes octaves. # Dans la notion de renversement, seule importe en fait la note de basse. Ainsi, les accords ''mi-sol-do'', ''mi-do-sol'', ''mi-do-mi-sol'', ''mi-sol-mi-do''… sont tous une déclinaison du premier renversement de ''do-mi-sol'' et ils seront abrégés de la même manière (''mi''<sup>6</sup> en musique classique ou C/E en musique populaire et jazz, voir plus bas). {{clear}} == Notation des accords de trois notes == Les accords de trois notes sont appelés « accords de quinte » en classique, et « triades » en jazz. [[Fichier:Progression dominante renverse parfait do majeur chiffrage.svg|vignette|upright=0.7|Chiffrage du second renversement d'un accord de ''sol'' majeur et d'un accord de ''do'' majeur : notation en musique populaire et jazz (haut) et notation de basse chiffrée (bas).]] Les accords sont construits de manière systématique. Nous pouvons donc les représenter de manière simplifiée. Cette notation simplifiée des accords est appelée « chiffrage ». Reprenons la progression d'accords ci-dessus : « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » dans la tonalité de ''do'' majeur. On utilise en général trois notations différentes : * en musique populaire, jazz, rock… un accord est désigné par sa note fondamentale ; ici donc, les accords sont notés « ''sol'' - ''do'' » ou, en notation anglo-saxonne, « G - C » ;<br /> comme le premier accord est renversé, on indique la note de basse après une barre, la progression d'accords est donc chiffrée '''« ''sol''/''ré'' - ''do'' »''' ou '''« G/D - C »''' ;<br /> il s'agit ici d'accords composés d'une tierce majeure et d'une quinte juste ; si les accords sont constitués d'intervalles différents, nous ajoutons un symbole après la note : « m » ou « – » si la tierce est mineure, « dim » ou « ° » si la quinte est diminuée ; * en musique classique, on utilise la notation de « basse chiffrée » (utilisée notamment pour noter la basse continue en musique baroque) : on indique la note de basse sur la portée et on lui adjoint l'intervalle de la fondamentale à la note la plus haute (donc ici respectivement 6 et 5, puisque ''sol''-''si'' est une sixte et ''do''-''sol'' est une quinte), étant sous-entendu que l'on a des empilements de tierce en dessous ; mais dans le cas du premier accord, le premier intervalle n'est pas une tierce, mais une quarte ''(ré''-''sol)'', on note donc '''« ''ré'' <sup>6</sup><sub>4</sub> - ''do'' <sup>5</sup> »'''<ref>quand on ne dispose pas de la notation en supérieur (exposant) et inférieur (indice), on utilise parfois une notation sous forme de fraction : ''sol'' 6/4 et ''do'' 5/.</ref> ; * lorsque l'on fait l'analyse d'un morceau, on s'attache à identifier la note fondamentale de l'accord (qui est différente de la basse dans le cas d'un renversement) ; on indique alors le degré de la fondamentale : '''« {{Times New Roman|V<sup>6</sup><sub>4</sub> - I<sup>5</sup>}} »'''. La notation de basse chiffrée permet de construire l'accord à la volée : * on joue la note indiquée (basse) ; * s'il n'y a pas de 2 ni de 4, on lui ajoute la tierce ; * on ajoute les intervalles indiqués par le chiffrage. La notation de musique jazz oblige à connaître la composition des différents accords, mais une fois que ceux-ci sont acquis, il n'y a pas besoin de reconstruire l'accord. La notation de basse chiffrée avec les chiffres romains n'est pas utilisée pour jouer, mais uniquement pour analyser ; Sur les partitions avec basse chiffrée, il y a simplement les chiffrages indiqués au-dessus de la partie de basse. Le chiffrage avec le degré en chiffres romains présente l'avantage d'être indépendant de la tonalité et donc de se concentrer sur la fonction de l'accord au sein de la tonalité. Par exemple, ci-dessous, nous pouvons parler de la progression d'accords « {{Times New Roman|V - I}} » de manière générale, cette notation étant valable quelle que soit la tonalité. [[File:Progression dominante renverse parfait do majeur chiffrage basse continue.svg|thumb|Chiffrage en notation basse chiffrée de la progression d'accords « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » en do majeur.]] {{note|En notation de base continue avec fondamentale en chiffres romains, la fondamentale est toujours indiquée ''sous'' la portée de la partie de basse. Les intervalles sont indiqués au-dessus de la portée de la partie de basse ; lorsque l'on fait une analyse, on peut ayssi les indiquer à côté du degré en chiffres romains, donc sous la portée de la basse.}} {{note|En notation rock, le 5 en exposant indique un accord incomplet avec uniquement la fondamentale et la quinte, un accord sans tierce appelé « accord de puissance » ou ''{{lang|en|power chord}}''. Par exemple, C<sup>5</sup> est l'accord ''do-sol''.}} {{clear}} [[Fichier:Accords parfait do majeur basse chiffree fondamental et renverse.svg|vignette|upright=2.5|Chiffrage de l'accord parfait de ''do'' majeur en basse chiffrée, à l'état fondamental et ses renversements.]] Concernant les accords parfaits en notation de basse chiffrée : * un accord parfait à l'état fondamental est chiffré « <sup>5</sup> » ; on l'appelle « accord de quinte » ; * le premier renversement est chiffré « <sup>6</sup> » (la tierce est implicite) ; on l'appelle « accord de sixte » ; * le second renversement est noté « <sup>6</sup><sub>4</sub> » ; on l'appelle « accord de sixte et de quarte » (ou bien « de quarte et de sixte »). Par exemple, pour l'accord parfait de ''do'' majeur : * l'état fondamental ''do''-''mi''-''sol'' est noté ''do''<sup>5</sup> (« ''do'' chiffré cinq ») ; * le premier renversement ''mi''-''sol''-''do'' est noté ''mi''<sup>6</sup> (« ''mi'' chiffré six ») ; * le second renversement ''sol''-''do''-''mi'' est noté ''sol''<sup>6</sup><sub>4</sub> (« ''sol'' chiffré six, quatre »). Il y a une exception : l'accord construit sur la sensible (7{{e}} degré) contient une quinte diminuée et non une quinte juste. Le chiffrage est donc différent : * l'état fondamental ''si''-''ré''-''fa'' est noté ''si''<sup><s>5</s></sup> (« ''si'' chiffré cinq barré »), « accord de quinte diminuée » ; * le premier renversement ''ré''-''fa''-''si'' est noté ''ré''<sup>+6</sup><sub>3</sub> (« ''ré'' chiffré plus six »), « accord de sixte sensible et tierce » ; * le second renversement ''fa''-''si''-''ré'' est noté ''fa''<sup>6</sup><sub>+4</sub> (« ''fa'' chiffré si, plus quatre »), « accord de sixte et quarte sensible ». Par ailleurs, on ne considère pas qu'il est fondé sur la sensible, mais sur la dominante. Si certains indiquent le degré {{Times New Roman|VII}}, d'autres indiquent le degré {{Times New Roman|V}} en mettant des guillemets, « {{Times New Roman|“V”}} ». Donc selon l'état, le chiffrage est {{Times New Roman|“V”<sup><s>5</s></sup>}}, {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}} ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}. En notation jazz, on ajoute « dim », « <sup>o</sup> » ou bien « <sup>♭5</sup> » au chiffrage, ici : B dim, B<sup>o</sup> ou B<sup>♭5</sup> pour l'état fondamental. Pour les renversements : B dim/D et B dim/F ; ou bien B<sup>o</sup>/D et B<sup>o</sup>/F ; ou bien B<sup>♭5</sup>/D et B<sup>♭5</sup>/F. {{clear}} [[Fichier:Accords basse chiffree basse do fondamental et renverses.svg|vignette|upright=2|Basse chiffrée : accords de quinte, de sixte et de sixte et de quarte ayant pour basse ''do''.]] Et concernant les accords ayant pour basse ''do'' en tonalité de ''do'' majeur : * l'accord ''do''<sup>5</sup> est un accord à l'état fondamental, c'est donc l'accord ''do''-''mi''-''sol'' (sa fondamentale est ''do'') ; * l'accord ''do''<sup>6</sup> est le premier renversement d'un accord, c'est donc l'accord ''do''-''mi''-''la'' (sa fondamentale est ''la'') ; * l'accord ''do''<sup>6</sup><sub>4</sub> est le second renversement d'un accord, c'est donc l'accord ''do''-''fa''-''la'' (sa fondamentale est ''fa''). {{clear}} == Notes étrangères == La musique européenne s'appuie essentiellement sur des accords parfaits, c'est-à-dire fondés sur une tierce majeure ou mineure, et une quinte juste. Il arrive fréquemment qu'un accord ne soit pas un accord parfait. Les notes qui font partie de l'accord parfait sont appelées « notes naturelles » et la note qui n'en fait pas partie est appelée « note étrangère ». Il existe plusieurs types de notes étrangères : * anticipation : la note étrangère est une note naturelle de l'accord suivant ; * appogiature : note d'ornementation qui se résout par mouvement conjoint, c'est-à-dire qu'elle est suivie par une note située juste au-dessus ou en dessous (seconde ascendante ou descendante) qui est, elle, une note naturelle ; * broderie : on part d'une note naturelle, on monte ou on descend d'une seconde, puis on revient sur la note naturelle ; * double broderie : on part d'une note naturelle, on joue la note du dessus puis la note du dessous avant de revenir à la note naturelle ; ou bien on joue la note du dessous puis la note du dessus ; * échappée : note étrangère n'appartenant à aucune des autres catégories ; * note de passage : mouvement conjoint allant d'une note naturelle d'un accord à une note naturelle de l'accord suivant ; * pédale : la note de basse reste la même pendant plusieurs accords successifs ; * retard : la note étrangère est une note naturelle de l'accord précédent. Les notes étrangères ne sont pas chiffrées. [[File:Notes etrangeres accords.svg|center|Différents types de notes étrangères.]] {{note|Les anglophones distinguent deux types de retard : la ''{{lang|en|suspension}}'' est résolue vers le haut (le mouvement est ascendant), le ''{{lang|en|retardation}}'' est résolu vers le bas (le mouvement est descendant).}} == Principaux accords == Les trois principaux accords sont : * l'accord parfait majeur : il est construit sur les degrés {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (médiante) et {{Times New Roman|V}} (dominante) d'une gamme majeure ; il est noté {{Times New Roman|I}}<sup>5</sup>, {{Times New Roman|IV}}<sup>5</sup>, {{Times New Roman|V}}<sup>5</sup> ; * l'accord parfait mineur : il est construit sur les degrés {{Times New Roman|I}} (tonique) et {{Times New Roman|IV}} (sous-tonique) d'une gamme mineure harmonique ; il est également noté {{Times New Roman|I}}<sup>5</sup> et {{Times New Roman|IV}}<sup>5</sup>, les anglo-saxons le notent {{Times New Roman|i}}<sup>5</sup> et {{Times New Roman|iv}}<sup>5</sup> (la minuscule indiquant le caractère mineur) ; * l'accord de septième de dominante : il est construit sur le degré {{Times New Roman|V}} (dominante) d'une gamme majeure ou mineure harmonique ; il est noté {{Times New Roman|V}}<sup>7</sup><sub>+</sub>. On peut trouver ces trois accords sur d'autres degrés, et il existe d'autre types d'accords. Nous verrons cela plus loin. {| class="wikitable" |+ Constitution des principaux accords — Dénomination classique |- ! scope="col" | Accord ! scope="col" | 1<sup>er</sup> intervalle ! scope="col" | 2<sup>e</sup> intervalle ! scope="col" | 3<sup>e</sup> intervalle |- ! scope="row" | Accord parfait majeur | tierce majeure (3M) || quinte juste (5J) || — |- ! scope="row" | Accord parfait mineur | tierce mineure (3m) || quinte juste (5J) || — |- ! scope="row" | Accord de septième de dominante | tierce majeure (3M) || quinte juste (5J) || septième mineure (7m) |} {| class="wikitable" |+ Constitution des principaux accords — Dénomination jazz |- ! scope="col" | Accord ! scope="col" | 1<sup>er</sup> intervalle ! scope="col" | 2<sup>e</sup> intervalle ! scope="col" | 3<sup>e</sup> intervalle |- ! scope="row" | Triade majeure | tierce majeure (3M) || quinte juste (5J) || — |- ! scope="row" | Triade mineure | tierce mineure (3m) || quinte juste (5J) || — |- ! scope="row" | Accord de septième | tierce majeure (3M) || quinte juste (5J) || septième mineure (7m) |} {| border="0" |- | [[Fichier:Accord do majeur arpege puis plaque.midi | Accord parfait de ''do'' majeur (C).]] || [[Fichier:Accord do mineur arpege puis plaque.midi | Accord parfait de ''do'' mineur (Cm).]] || [[Fichier:Accord do septieme arpege puis plaque.midi | Accord de septième de dominante de ''fa'' majeur (C<sup>7</sup>).]] |- | Accord parfait<br /> de ''do'' majeur (C). || Accord parfait<br /> de ''do'' mineur (Cm). || Accord de septième de dominante<br /> de ''fa'' majeur (C<sup>7</sup>). |} '''Rappel :''' * la tierce mineure est composée d'un ton et demi (1 t ½) ; * la tierce majeur est composée de deux tons (2 t) ; * la quinte juste a la même altération que la fondamentale, sauf lorsque la fondamentale est ''si'' (la quinte juste est alors ''fa''♯) ; * la septième mineure est le renversement de la seconde majeure (1 t). [[File:Renversements accords pft fa maj basse chiffree.svg|thumb|Renversements de l'accord parfait de ''fa'' majeur, et la notation de basse chiffrée.]] [[File:Renversements accord sept de dom fa maj basse chiffree.svg|thumb|Renversements de l'accord de septième de dominante de ''fa'' majeur, et la notation de basse chiffrée.]] {| class="wikitable" |+ Notation des principaux accords en musique classique |- ! scope="col" | Accord ! scope="col" | État<br /> fondamental ! scope="col" | Premier<br /> renversement ! scope="col" | Deuxième<br /> renversement ! scope="col" | Troisième<br /> renversement |- ! scope="row" | Accord parfait | {{Times New Roman|I<sup>5</sup>}}<br/> acc. de quinte || {{Times New Roman|I<sup>6</sup>}}<br :> acc. de sixte || {{Times New Roman|I<sup>6</sup><sub>4</sub>}}<br /> acc. de quarte et de sixte || — |- ! scope="row" | Accord de septième<br /> de dominante | {{Times New Roman|V<sup>7</sup><sub>+</sub>}}<br /> acc.de septième de dominante || {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}<br />acc. de sixte et quinte diminuée || {{Times New Roman|V<sup>+6</sup>}}<br />acc. de sixte sensible || {{Times New Roman|V<sup>+4</sup>}}<br />acc. de quarte sensible<br />acc. de triton |} {| class="wikitable" |+ Notation des principaux accords en jazz |- ! scope="col" | Accord ! scope="col" | Chiffrage ! scope="col" | Renversements |- ! scope="row" | Triade majeure | X | rowspan="3" | Les renversements se notent en mettant la basse après une barre de fraction, par exemple pour la triade de ''do'' majeur : * état fondamental : C ; * premier renversement : C/E ; * second renversement : C/G. |- ! scope="row" | Triade mineure | Xm, X– |- ! scope="row" | Septième | X<sup>7</sup> |} {{clear}} Dans le cas d'un accord de septième de dominante, le nom de l'accord change selon que l'on est en musique classique ou en jazz : en musique classique, on donne le nom de la tonalité alors qu'en jazz, on donne le nom de la fondamentale. Ainsi, l'accord appelé « septième de dominante de ''do'' majeur » en musique classique, est appelé « ''sol'' sept » (G<sup>7</sup>) en jazz : la dominante (degré {{Times New Roman|V}}, dominante) de la tonalité de ''do'' majeur est la note ''sol''. Comment appelle-t-on en musique classique l'accord appelé « ''do'' sept » (C<sup>7</sup>) en jazz ? Les tonalités dont le ''do'' est la dominante sont les tonalités de ''fa'' majeur (''si''♭ à la clef) et de ''fa'' mineur harmonique (''si''♭, ''mi''♭, ''la''♭ et ''ré''♭ à la clef et ''mi''♮ accidentel). Il s'agit donc de l'accord de septième de dominante des tonalités de ''fa'' majeur et ''fa'' mineur harmonique. {| class="wikitable" |+ Accords fréquents pour quelques la tonalités majeures ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Accord parfait<br />{{Times New Roman|I<sup>5</sup>}} ! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}} |- |''Do'' majeur || || C<br />''do-mi-sol'' || G7<br />''sol-si-ré-fa'' |- |''Sol'' majeur || ''fa''♯ || G<br />''sol-si-ré'' || D7<br />''ré-fa''♯''-la-do'' |- |''Ré'' majeur || ''fa''♯, ''do''♯ || D<br />''ré-fa''♯''-la'' || A7<br />''la-do''♯''-mi-sol'' |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A<br />''la-do''♯''-mi'' || E7<br />''mi-sol''♯''-si-ré'' |- | ''Fa'' majeur || ''si''♭ || F<br />''fa-la-do'' || C7<br />''do-mi-sol-si''♭ |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭<br />''si''♭''-ré-fa'' || F7<br />''fa-la-do-mi''♭ |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭<br />''mi''♭''-sol-si''♭ || B♭7<br />''si''♭''-ré-fa-la''♭ |} {| class="wikitable" |+ Accords fréquents pour quelques la tonalités mineures harmoniques ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Accord parfait<br />{{Times New Roman|i<sup>5</sup>}} ! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}} |- |''La'' mineur<br />harmonique || || Am, A–<br />''la-do-mi'' || E7<br />''mi-sol''♯''-si-ré'' |- |''Mi'' mineur<br />harmonique || ''fa''♯ || Em, E–<br />''mi-sol-si'' || B7<br />''si-ré''♯''-fa''♯''-la'' |- |''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || Bm, B–<br />''si-ré-fa''♯ || F♯7<br />''fa''♯''la''♯''-do''♯''-mi'' |- |''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || F♯m, F♯–<br />''fa''♯''-la-do''♯ || C♯7<br />''do''♯''-mi''♯''-sol''♯''-si'' |- | ''Ré'' mineur<br />harmonique || ''si''♭ || Dm, D–<br />''ré-fa-la'' || A7<br />''la-do''♯''-mi-sol'' |- | ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || Gm, G–<br />''sol-si''♭''-ré'' || D7<br />''ré-fa''♯''-la-do'' |- | ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || Cm, C–<br />''do-mi''♭''-sol'' || G7<br />''sol-si''♮''-ré-fa'' |} {{clear}} == Accords sur les degrés d'une gamme == === Harmonisation d'une gamme === [[Fichier:Accord trois notes gamme do majeur chiffre.svg|vignette|upright=1.2|Accords de trois note sur la gamme de ''do'' majeur, chiffrés.]] On peut ainsi construire une triade par degré d'une gamme. Pour une gamme majeure, les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|V<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|II<sup>5</sup>}}, {{Times New Roman|III<sup>5</sup>}}, {{Times New Roman|VI<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|ii<sup>5</sup>}}, {{Times New Roman|iii<sup>5</sup>}}, {{Times New Roman|vi<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}. Les accords ont tous une quinte juste à l'exception de l'accord {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} qui a une quinte diminuée, raison pour laquelle le « 5 » est barré. C'est un accord dit « de quinte diminuée ». En jazz, l'accord diminué est noté « dim », « ° », « m<sup>♭5</sup> » ou « <sup>–♭5</sup> ». Nous avons donc trois types d'accords (dans la notation jazz) : X (triade majeure), Xm (triade mineure) et X° (triade diminuée), la lettre X remplaçant le nom de la note fondamentale. {{clear}} [[Fichier:Accord trois notes gamme la mineur chiffre.svg|vignette|upright=1.2|Accords de trois notes sur une gamme de ''la'' mineur harmonique, chiffrés.]] Pour une gamme mineure harmonique, les accords {{Times New Roman|III<sup>+5</sup>}}, {{Times New Roman|V<sup>♯</sup>}} et {{Times New Roman|VI<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|II<sup><s>5</s></sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|i<sup>5</sup>}}, {{Times New Roman|ii<sup><s>5</s></sup>}}, {{Times New Roman|iv<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}. Les accords {{Times New Roman|ii<sup><s>5</s></sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} ont une quinte diminuée ; ce sont des accords dits « de quinte diminuée ». L'accord {{Times New Roman|III<sup>+5</sup>}} a une quinte augmentée ; le signe « plus » indique que la note de cinquième, le ''sol'' dièse, est la sensible. En jazz, l'accord est noté « aug » ou « <sup>+</sup> ». Les autres accords ont une quinte juste. Aux trois accords générés par une gamme majeure (X, Xm et X°), nous voyons ici apparaître un quatrième type d'accord : la triade augmentée X<sup>+</sup>. Nous remarquons que des gammes ont des accords communs. Par exemple, l'accord {{Times New Roman|ii<sup>5</sup>}} de ''do'' majeur est identique à l'accord {{Times New Roman|iv<sup>5</sup>}} de ''la'' mineur (il s'agit de l'accord Dm). Quel que soit le mode, les accords construits sur la sensible (accord de quinte diminuée) sont rarement utilisés. S'ils le sont, c'est en tant qu'accord de septième de dominante sans fondamentale (voir ci-après). C'est la raison pour laquelle le chiffrage indique le degré {{Times New Roman|V}} entre guillemets, et non pas le degré {{Times New Roman|VII}} (mais pour des raisons de clarté, nous l'indiquons entre parenthèses au début). En mode mineur, l'accord de quinte augmentée {{Times New Roman|iii<sup>+5</sup>}} est très peu utilisé (voir plus loin ''[[#Progression_d'accords|Progression d'accords]]''). C'est un accord considéré comme dissonant. On voit que : * un accord parfait majeur peut appartenir à cinq gammes différentes ;<br /> par exemple l'accord parfait de ''do'' majeur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> degré de la gamme de ''do'' majeur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' mineur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''mi'' mineur ; * un accord parfait mineur peut appartenir à cinq gammes différentes ;<br />par exemple l'accord parfait de ''la'' mineur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> de la gamme de ''la'' mineur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''mi'' mineur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|III}}<sup>e</sup> degré de ''fa'' majeur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''do'' majeur ; * un accord de quinte diminuée peut appartenir à trois gammes différentes ;<br />par exemple, l'accord de quinte diminuée de ''si'' est l'accord construit sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' majeur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''la'' mineur et sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' mineur ; * un accord de quinte augmentée (à l'état fondamental) ne peut appartenir qu'à une seule gamme ;<br /> par exemple, l'accord de quinte augmentée de ''do'' est l'accord construit sur le {{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur. {| class="wikitable" |+ Notation jazz des triades |- | rowspan="2" colspan="2" | ! scope="col" colspan="2" | Tierce |- ! scope="col" | 3m ! scope="col" | 3M |- ! rowspan="3" | Quinte ! scope="row" | 5d | Xᵒ, X<sub>m</sub><sup>(♭5)</sup> || |- ! scope="row" | 5J | Xm, X– || X |- ! scope="row" | 5A | || X+, X<sup>(♯5)</sup> |} === Harmonisation par des accords de septième === [[Fichier:Harmonisation gamme do majeur par septiemes chiffre.svg|vignette|upright=2|Harmonisation de la gamme de do majeur par des accords de septième.]] Les accords de septième contiennent une dissonance et créent ainsi une tension. Ils sont très utilisés en jazz. Nous avons représenté ci-contre l'harmonisation de la gamme de ''do'' majeur. La constitution des accords est la suivantes : * tierce majeure (3M) ** quinte juste (5J) *** septième mineure (7m) : sur le degré V, c'est l'accord de septième de dominante V<sup>7</sup><sub>+</sub>, noté X<sup>7</sup> (X pour G), *** septième majeure (7M) : sur les degrés I et IV, appelés « accords de septième majeure » et notés aussi X<sup>maj7</sup> ou X<sup>Δ</sup> (X pour C ou F) ; * tierce mineure (3m) ** quinte juste (5J) *** septième mineure : sur les degrés ii, iii et vi, appelés « accords mineur septième » et notés Xm<sup>7</sup> ou X–<sup>7</sup> (X pour D, E ou A), ** quinte diminuée (5d) *** septième mineure (7m) : sur le degré vii, appelé « accord demi-diminué » (puisque seule la quinte est diminuée) et noté X<sup>∅</sup> ou Xm<sup>7(♭5)</sup> ou X–<sup>7(♭5)</sup> (X pour B) ;<br /> en musique classique, on considère que c'est un accord de neuvième de dominante sans fondamentale. Nous avons donc quatre types d'accords : X<sup>7</sup>, X<sup>maj7</sup>, Xm<sup>7</sup> et X<sup>∅</sup> En jazz, on ajoute souvent la quarte à l'accord de sous-dominante IV (sur le ''fa'' dans une gamme de ''do'' majeur) ; il s'agit ici d'une quarte augmentée (''fa''-''si'') et l'accord est surnommé « accord lydien » mais cette dénomination est erronée (il s'agit d'une mauvaise interprétation de textes antiques). C'est un accord de onzième sans neuvième (la onzième étant l'octave de la quarte), il est noté X<sup>maj7(♯11)</sup> ou X<sup>Δ(♯11)</sup> (ici, F<sup>maj7(♯11)</sup>, ''fa''-''la''-''do''-''mi''-''si'' ou ''fa''-''la''-''si''-''do''-''mi''). {| class="wikitable" |+ Chiffrage jazz des accords de septième |- ! scope="col" rowspan="2" | Tierce ! scope="col" rowspan="2" | Quinte ! scope="col" colspan="2" | Septième |- ! scope="col" | 7m ! scope="col" | 7M |- | rowspan="2" | 3m | 5d || X<sup>∅</sup>, X<sub>m</sub><sup>7(♭5)</sup>, X–<sup>7(♭5)</sup> || |- | rowspan="2" | 5J | X<sub>m</sub><sup>7</sup>, X–<sup>7</sup> || |- | rowspan="2" | 3M | X<sup>7</sup> || X<sup>maj7</sup>, X<sup>Δ</sup> |- | 5A || || X<sub>+</sub><sup>maj7</sup>, X<sub>+</sub><sup>Δ</sup> |} === Modulation et emprunt === Un morceau peut comporter des changements de tonalité; appelés « modulation ». Il y a parfois un court passage dans une tonalité différente, typiquement sur une ou deux mesures, avant de retourner dans la tonalité d'origine : on parle d'emprunt. Lorsqu'il y a une modulation ou un emprunt, les degrés changent. Un même accord peut donc avoir une fonction dans une partie du morceau et une autre fonction ailleurs. L'utilisation d'accord différents, et en particulier d'accord utilisant des altérations accidentelles, indique clairement une modulation. Nous avons vu précédemment que les modulations courantes sont : * les modulations dans les tons voisins ; * les modulations homonymes ; * les marches harmoniques. Une modulation entre une tonalité majeure et mineure change la couleur du passage, * la modulation la plus « douce » est entre les tonalités relatives (par exemple''do'' majeur et ''la'' mineur) car ces tonalités utilisent quasiment les mêmes notes ; * la modulation la plus « voyante » est la modulation homonyme (par exemple entre ''do'' majeur et ''do'' mineur). Une modulation commence souvent sur l'accord de dominante de la nouvelle tonalité. Pour analyser un œuvre, ou pour improviser sur une partie, il est important de reconnaître les modulations. La description de la successind es tonalités s'appelle le « parcours tonal ». === Exercices élémentaires === L'apprentissage des accords passe par quelques exercices élémentaires. '''1. Lire un accord''' Il s'agit de lecture de notes : des notes composant les accords sont écrites « empilées » sur une portée, il faut les lire en énonçant les notes de bas en haut. '''2. Reconnaître la « couleur » d'un accord''' On écoute une triade et il faut dire si c'est une triade majeure ou mineure. Puis, on complexifie l'exercice en ajoutant la septième. '''3. Chiffrage un accord''' Trouver le nom d'un accord à partir des notes qui le composent. '''4. Réalisation d'un accord''' Trouver les notes qui composent un accord à partir de son nom. '''5. Dictée d'accords''' On écoute une succession d'accords et il faut soit écrire les notes sur une portée, soit écrire les noms de accords. [[File:Exercice constitution accord basse chiffree.svg|thumb|Exercice : constitution d'accord à partir de la basse chiffrée.]] '''Exercices de basse chiffrée''' ''Réalisation d'un accord'' Sur la figure suivante, écrire les notes des accords correspondant à la basse chiffrée. Déterminer le degré de la fondamentale pour chaque accord en considérant que nous sommes dans la tonalité de ''sol'' majeur. {{boîte déroulante/début|titre=Solution}} [[File:Exercice constitution accord basse chiffree solution.svg|vignette|Solution.]] # La note de basse est un ''do''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''mi'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''sol''.<br />Le chiffrage « <sup>5</sup> » indique que c'est un accord dans son état fondamental (l'écart entre deux notes consécutives ne dépasse pas la tierce), la fondamentale est donc la basse, ''do'', qui est le degré IV de la tonalité. # La note de basse est un ''si''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''ré'', puis nous appliquons le chiffrage 6 et ajoutons la sixte, ''sol''.<br />Le chiffrage « <sup>6</sup> » indique que c'est un accord dans son premier renversement. En le remettant dans son état fondamental, nous obtenons ''sol-si-ré'', la fondamentale est donc la tonique, le degré I. # La note de basse est un ''la''. Nous ajoutons la tierce (chiffre 3), ''do'', et la sixte (6), ''fa''♯. Nous vérifions que le ''fa''♯ est la sensible (signe +)<br />Nous voyons un « blanc » entre les notes ''do'' et ''fa''♯. En descendant le ''fa''♯ à l'octave inférieure, nous obtenons un empilement de tierces ''fa''♯-''la-do'', le fondamentale est donc ''fa''♯, le degré VII. Nous pouvons le voir comme le deuxième renversement de l'accord de septième de dominante, sans fondamentale. # La note de basse est un ''fa''♯. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''la'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''do'' ; nous vérifions qu'il s'agit bien d'une quinte diminuée (le 5 est barré). Nous appliquons le chiffre 6 et ajoutons la sixte, ''ré''.<br />Nous voyons que les notes ''do'' et ''ré'' sont conjointes (intervalle de seconde). En descendant le ''ré'' à l'octave inférieure, nous obtenons un empilement de tierces ''ré-fa''♯-''la-do'', le fondamentale est donc ''ré'', le degré V. Nous constatons que l'accord chiffré est le premier renversement de l'accord de septième de dominante. {{boîte déroulante/fin}} {{clear}} ''Chiffrage d'accords'' [[Fichier:Exercice chiffrage accord basse chiffree.svg|vignette|Accords à chiffrer.]] Chiffrer les accords ci-contre. {{boîte déroulante/début|titre=Solution}} [[File:Exercice chiffrage accord basse chiffree solution.svg|vignette|Solution.]] # On relève les intervalles en partant de la basse : tierce majeure (3M) et quinte juste (5J). Le chiffrage complet est donc ''fa''<sup>5</sup><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''fa''<sup>5</sup>.<br /> On peut aussi reconnaître que c'est l'accord parfait sur la tonique de la tonalité de ''fa'' majeur dans son état fondamental, le chiffrage d'un accord parfait étant <sup>5</sup>. # On relève les intervalles en partant de la basse : quarte juste (4J), sixte majeure (6M). Le chiffrage complet est donc ''fa''<sup>6</sup><sub>4</sub>.<br /> On peut aussi reconnaître que c'est le second renversement de l'accord ''mi-sol-si'', sur la tonique de la tonalité de ''mi'' mineur, le chiffrage du second renversement d'un accord parfait étant <sup>6</sup><sub>4</sub>. # Les intervalles en partant de la basse sont : tierce mineure (3m), quinte diminuée (5d), sixte mineure (6m). Le chiffrage complet est donc ''mi''<sup>6</sup><small><s>5</s></small><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''mi''<sup>6</sup><sub><s>5</s></sub>.<br /> On reconnaît le premier renversement de l'accord ''do-mi-sol-si''♭, accord de septième de dominante de la tonalité de ''fa'' majeur. # Les intervalles en partant de la basse sont : tierce mineure (3m), quinte juste (5J), septième mineure (7m). Le chiffrage complet est donc ''ré''<sup>7</sup><small>5</small><sub>3</sub> ; c'est typique d'un accord de septième de dominante, son chiffrage est donc ''ré''<sup>7</sup><sub>+</sub>.<br /> On reconnaît l'accord de septième de dominante de la tonalité de ''sol'' mineur dans son état fondamental. {{boîte déroulante/fin}} {{clear}} [[File:Exercice constitution accord notation jazz.svg|thumb|Exercice : constitution d'un accord d'après son chiffrage en notation jazz.]] '''Exercices de notation jazz''' ''Réalisation d'un accord'' Sur la figure suivante, écrire les notes des accords correspondant aux chiffrages. {{boîte déroulante/début|titre=Solution}} [[File:Exercice constitution accord notation jazz solution.svg|thumb|Solution.]] # Il s'agit de la triade majeure de ''do'' dans son état fondamental. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''do-mi-sol''. # Il s'agit de la triade majeure de ''sol''. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''sol-si-ré''. On renverse l'accord afin que la basse soit le ''si'', l'accord est donc ''si-ré-sol''. # Il s'agit de l'accord demi-diminué de ''fa''♯. Les intervalles sont la tierce mineure (3m), la quinte diminuée (5d) et la septième mineure (7m). Les notes sont donc ''fa''♯-''la-do-mi''. Nous renversons l'accord afin que la basse soit le ''la'', l'accord est donc ''a-do-mi-fa''♯. # Il s'agit de l'accord de septième de ''ré''. Les intervalles sont donc la tierce majeure (3M), la quinte juste (5J) et la septième mineure (7m). Les notes sont ''ré-fa''♯''-la-do''. Nous renversons l'accord afin que la basse soit le ''fa''♯, l'accord est donc ''fa''♯''-la-do-ré''. {{boîte déroulante/fin}} {{clear}} ''Chiffrage d'accords'' [[File:Exercice chiffrage accord notation jazz.svg|thumb|Accords à chiffrer.]] Chiffrer les accords ci-contre. {{boîte déroulante/début|titre=Solution}} [[File:Exercice chiffrage accord notation jazz solution.svg|thumb|Solution.]] # Les notes sont toutes sur des interlignes consécutifs, c'est donc un empilement de tierces ; l'accord est dans son état fondamental. Les intervalles sont une tierce majeure (''fa-la'' : 3M) et une quinte juste (''fa-do'' : 5J), c'est donc la triade majeure de ''fa''. Le chiffrage est F. # Il y a un blanc dans l'empilement des notes, c'est donc un accord renversé. En permutant les notes pour n'avoir que des tierces, on trouve l'accord ''mi-sol-si''. Les intervalles sont une tierce mineure (''mi-sol'' : 3m) et une quinte juste (''mi-si'' : 5J), c'est donc la triade mineure de ''mi'' avec un ''si'' à la basse. Le chiffrage est Em/B ou E–/B. # Il y deux notes conjointes, c'est donc un renversement. L'état fondamental de cet accord est ''do-mi-sol-si''♭. Les intervalles sont une tierce majeure (''do-mi'' : 3M), une quinte juste (''do-sol'' : 5J) et une septième mineure (''do-si''♭ : 7m). C'est donc l'accord de ''do'' septième avec un ''mi'' à la basse, chiffré C<sup>7</sup>/E. # Les notes sont toutes sur des interlignes consécutifs, l'accord est dans son état fondamental. Les intervalles sont la tierce mineure (''ré-fa'' : 3m), une quinte juste (''ré-la'' : 5J) et une septième mineure (''ré-do'' : 7m). C'est donc l'accord de ''ré'' mineur septième, chiffré Dm<sup>7</sup> ou D–<sup>7</sup>. {{boîte déroulante/fin}} {{clear}} == Harmonie fonctionnelle == Le choix des accords et de leur succession — la progression des accords — est un élément important d'un morceau, de sa composition. Le compositeur ou la compositrice a bien sûr une liberté totale, mais pour faire des choix, il faut comprendre les conséquences de ces choix, et donc ici, les effets produits par les accords et leur progression. Une des manières d'aborder le sujet est l'harmonie fonctionnelle. === Les trois fonctions des accords === En harmonie tonale, on considère que les accords ont une fonction. Il existe trois fonctions : * la fonction de tonique, {{Times New Roman|I}} ; * la fonction de sous-dominante, {{Times New Roman|IV}} ; * la fonction de dominante, {{Times New Roman|V}}. L'accord de tonique, {{Times New Roman|I}}, est l'accord « stable » de la tonalité par excellence. Il conclut en général les morceaux, et ouvre souvent les morceaux ; il revient fréquemment au cours du morceau. L'accord de dominante, {{Times New Roman|V}}, est un accord qui introduit une instabilité, une tension. En particulier, il contient la sensible (degré {{Times New Roman|VI}}), qui est une note « aspirée » vers la tonique. Cette tension, qui peut être renforcée par l'utilisation d'un accord de septième, est fréquemment résolue par un passage vers l'accord de tonique. Nous avons donc deux mouvements typiques : {{Times New Roman|I}} → {{Times New Roman|V}} (création d'une tension, d'une attente) et {{Times New Roman|V}} → {{Times New Roman|I}} (résolution d'une tension). Les accords de tonique et de dominante ont le cinquième degré en commun, cette note sert donc de pivot entre les deux accords. L'accord de sous-dominante, {{Times New Roman|IV}}, est un accord qui introduit lui aussi une tension, mais moins grande : il ne contient pas la sensible. Notons que s'il est une quarte au-dessus de la tonique, il est aussi une quinte en dessous d'elle ; il est symétrique de l'accord de dominante. Il a donc un rôle similaire à l'accord de dominante, mais atténué. L'accord de sous-dominante aspire soit vers l'accord de dominante, très proche, et l'on a alors une augmentation de la tension ; soit vers l'accord de tonique, un retour vers la stabilité (il a alors un rôle semblable à la dominante). Du fait de ces deux bifurcations possibles — augmentation de la tension ({{Times New Roman|IV}} → {{Times New Roman|V}}) ou retour à la stabilité ({{Times New Roman|IV}} → {{Times New Roman|I}}) —, l'utilisation de l'accord de sous-dominante introduit un certain flottement : si l'on peut facilement prédire l'accord qui suit un accord de dominante, on ne peut pas prédire ce qui suit un accord de sous-dominante. Notons que la composition ne consiste pas à suivre ces règles de manière stricte, ce qui conduirait à des morceaux stéréotypés et plats. Le plaisir d'écoute joue sur une alternance entre satisfaction d'une attente (respect des règles) et surprise (rompre les règles). === Accords remplissant ces fonctions === Les accords sur les autres degrés peuvent se ramener à une de ces trois fonctions : * {{Times New Roman|II}} : fonction de sous-dominante {{Times New Roman|IV}} ; * {{Times New Roman|III}} (très peu utilisé en mode mineur en raison de sa dissonance) et {{Times New Roman|VI}} : fonction de tonique {{Times New Roman|I}} ; * {{Times New Roman|VII}} : fonction de dominante {{Times New Roman|V}}. En effet, les accords étant des empilements de tierces, des accords situés à une tierce l'un de l'autre — {{Times New Roman|I}} ↔ {{Times New Roman|III}}, {{Times New Roman|II}} ↔ {{Times New Roman|IV}}, {{Times New Roman|V}} ↔ {{Times New Roman|VII}}, {{Times New Roman|VI}} ↔ {{Times New Roman|VIII}} ( = {{Times New Roman|I}}) — ont deux notes en commun. On retrouve le fait que l'accord sur le degré {{Times New Roman|VII}} est considéré comme un accord de dominante sans tonique. En mode mineur, l'accord sur le degré {{Times New Roman|III}} est évité, il n'a donc pas de fonction. {|class="wikitable" |+ Fonction des accords |- ! scope="col" | Fondamentale ! scope="col" | Fonction |- | {{Times New Roman|I}} || tonique |- | {{Times New Roman|II}} || sous-dominante faible |- | {{Times New Roman|III}} || tonique faible |- | {{Times New Roman|IV}} || sous-dominante |- | {{Times New Roman|V}} || dominante |- | {{Times New Roman|VI}} || tonique faible |- | {{Times New Roman|VII}} || dominante faible |} Par exemple en ''do'' majeur : * fonction de tonique : '''''do''<sup>5</sup> (C)''', ''mi''<sup>5</sup> (E–), ''la''<sup>5</sup> (A–) ; * fonction de sous-dominante : '''''fa''<sup>5</sup> (F)''', ''ré''<sup>5</sup> (D–) ; * fonction de dominante : '''''sol''<sup>5</sup> (G)''' ou ''sol''<sup>7</sup><sub>+</sub> (G<sup>7</sup>), ''si''<sup> <s>5</s></sup> (B<sup>o</sup>). En ''la'' mineur harmonique : * fonction de tonique : '''''la''<sup>5</sup> (A–)''', ''fa''<sup>5</sup> (F) [, rarement : ''do''<sup>+5</sup> (C<sup>+</sup>)] ; * fonction de sous-dominante : '''''ré''<sup>5</sup> (D–)''', ''si''<sup> <s>5</s></sup> (B<sup>o</sup>) ; * fonction de dominante : '''''mi''<sup>5</sup> (E)''' ou ''mi''<sup>7</sup><sub>+</sub> (E<sup>7</sup>), ''sol''♯<sup> <s>5</s></sup> (G♯<sup>o</sup>). Le fait d'utiliser des accords différents pour remplir une fonction permet d'enrichir l'harmonie, et de jouer sur l'équilibre entre satisfaction d'une attente (on respecte les règles sur les fonctions) et surprise (mais on n'utilise pas l'accord attendu). === Les dominantes secondaires === On utilise aussi des accords de septième dominante se fondant sur un autre degré que la dominante de la gamme ; on parle de « dominante secondaire ». Typiquement, avant un accord de septième de dominante, on utilise parfois un accord de dominante de dominante, dont le degré est alors noté « {{Times New Roman|V}} de {{Times New Roman|V}} » ou « {{Times New Roman|V}}/{{Times New Roman|V}} » ; la fondamentale est de l'accord est alors situé cinq degrés au-dessus de la dominante ({{Times New Roman|V}}), c'est donc le degré {{Times New Roman|IX}}, c'est-à-dire le degré {{Times New Roman|II}} de la tonalité en cours). Ou encore, on utilise un accord de dominante du degré {{Times New Roman|IV}} (« {{Times New Roman|V}} de {{Times New Roman|IV}} », la fondamentale est alors le degré {{Times New Roman|I}}) avant un accord sur le degré {{Times New Roman|IV}} lui-même. Par exemple, en tonalité de ''do'' majeur, on peut trouver un accord ''ré - fa''♯'' - la - do'' (chiffré {{Times New Roman|V}} de {{Times New Roman|V}}<sup>7</sup><sub>+</sub>), avant un accord ''sol - si - ré - fa'' ({{Times New Roman|V}}<sup>7</sup><sub>+</sub>). L'accord ''ré - fa''♯'' - la - do'' est l'accord de septième de dominante des tonalités de ''sol''. Dans la même tonalité, on pourra utiliser un accord ''do - mi - sol - si''♭ ({{Times New Roman|V}} de {{Times New Roman|IV}}<sup>7</sup><sub>+</sub>) avant un accord ''fa - la - do'' ({{Times New Roman|IV}}<sup>5</sup>). Le recours à une dominante secondaire peut atténuer une transition, par exemple avec un enchaînement ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> → ''fa''<sup>5</sup> (C → C<sup>7</sup> → F) qui correspond à un enchaînement {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}} : le passage ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> (C → C<sup>7</sup>) se fait en ajoutant une note (le ''si''♭) et rend naturel le passage ''do'' → ''fa''. Sur les sept degré de la gamme, on ne considère en général que cinq dominantes secondaires : en effet, la dominante du degré {{Times New Roman|I}} est la dominante « naturelle, primaire » de la tonalité (et n'est donc pas secondaire) ; et utiliser la dominante de {{Times New Roman|VII}} consisterait à considérer l'accord de {{Times New Roman|VII}} comme un accord propre, on évite donc les « {{Times New Roman|V}} de “{{Times New Roman|V}}” » (mais les « “{{Times New Roman|V}}” de {{Times New Roman|V}} » sont tout à fait « acceptables »). === Enchaînements classiques === Nous avons donc vu que l'on trouve fréquemment les enchaînements suivants : * pour créer une instabilité : ** {{Times New Roman|I}} → {{Times New Roman|V}}, ** {{Times New Roman|I}} → {{Times New Roman|IV}} (instabilité moins forte mais incertitude sur le sens d'évolution) ; * pour maintenir l'instabilité : ** {{Times New Roman|IV}} → {{Times New Roman|V}} ; * pour résoudre l'instabilité : ** {{Times New Roman|IV}} → {{Times New Roman|I}}, ** {{Times New Roman|V}} → {{Times New Roman|I}}, cas particuliers (voir plus bas) : *** {{Times New Roman|V}}<sup>+4</sup> → {{Times New Roman|I}}<sup>6</sup>, *** {{Times New Roman|I}}<sup>6</sup><sub>4</sub> → {{Times New Roman|V}}<sup>7</sup><sub>+</sub> → {{Times New Roman|I}}<sup>5</sup>. Les degrés indiqués ci-dessus sont les fonctions ; on peut donc utiliser les substitutions suivantes : * {{Times New Roman|I}} par {{Times New Roman|VI}} et, en tonalité majeure, {{Times New Roman|III}} ; * {{Times New Roman|IV}} par {{Times New Roman|II}} ; * {{Times New Roman|V}} par {{Times New Roman|VII}}. Pour enrichir l'harmonie, on peut utiliser les dominantes secondaires, en particulier : * {{Times New Roman|V}} de {{Times New Roman|V}} ({{Times New Roman|II}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|V}}, * {{Times New Roman|V}} de {{Times New Roman|IV}} ({{Times New Roman|I}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|IV}}. On peut enchaîner les enchaînements, par exemple {{Times New Roman|I}} → {{Times New Roman|IV}} → {{Times New Roman|V}}, ou encore {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}}… En jazz, on utilise très fréquemment l'enchaînement {{Times New Roman|II}} → {{Times New Roman|V}} → {{Times New Roman|I}} (deux-cinq-un). On peut bien sûr avoir d'autres enchaînements, mais ces règles permettent d'analyser un grand nombre de morceaux, et donnent des clefs utiles pour la composition. Nous voyons ci-après un certain nombre d'enchaînements courants dans différents styles == Exercice == Un hautboïste travaille la sonate en ''do'' mineur S. 277 de Heinichen. Sur le deuxième mouvement ''Allegro'', il a du mal à travailler un passage en raison des altérations accidentelles. Sur la suggestion de sa professeure, il décide d'analyser la progression d'accords sous-jacente afin que les altérations deviennent logiques. Il s'agit d'un duo hautbois et basson pour lequel les accords ne sont pas chiffrés, le basson étant ici un instrument soliste et non pas un élément de la basse continue. Sur l'extrait suivant, déterminez les basses et la qualité (chiffrage) des accords sous-jacents. Commentez. [[Fichier:Sonate hautbois basson heinichen 2e mvt mes49.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen.]] {{note|L'œuvre est en ''do'' mineur et devrait donc avoir trois bémols à la clef, or ici il n'y en a que deux. En effet, le ''la'' pouvant être bécarre en mode mineur mélodique ascendant, le compositeur a préféré le noter explicitement en altération accidentelle lorsque l'on est en mode mélodique naturel, harmonique ou mélodique descendant. C'est un procédé assez courant à l'époque baroque.}} {{boîte déroulante/début|titre=Solution}} Une des difficultés ici est que les arpèges joués par les instruments sont agrémentés de notes de passage. Les notes de la basse (du basson) sont différentes entre le premier et le deuxième temps de chaque mesure et ne peuvent pas appartenir au même accord. On a donc un accord par temps. Sur le premier temps de chaque mesure, le basson joue une octave. La note concernée est donc la basse de chaque accord. Pour savoir s'il s'agit d'un accord à l'état fondamental ou d'un renversement, on regarde ce que joue le hautbois : dans un mouvement conjoint (succession d'intervalles de secondes), il est difficile de distinguer les notes de l'arpège des notes de passage, mais : les notes des grands intervalles font partie de l'accord. Ainsi, sur le premier temps de la première mesure (la basse est un ''mi''♭), on a une sixte descendante ''sol''-''si''♭ et, à la fin du temps, une tierce descendante ''sol''-''mi''♭. L'accord est donc ''mi''♭-''sol''-''si''♭, c'est un accord de quinte (accord parfait à l'état fondamental). À la fin du premier temps, le basson joue un ''do'', c'est donc une note étrangère. Sur le second temps de la première mesure, le basson joue une tierce ascendante ''fa''-''la''♭, la première note est la basse de l'accord et la seconde une des notes de l'accord. Le hautbois commence par une sixte descendante ''la''♭-''do'', l'accord est donc ''fa''-''la''♭-''do'', un accord de quinte (accord parfait à l'état fondamental). Le ''do'' du basson la fin du premier temps est donc une anticipation. Les autres notes étrangères de la première mesure sont des notes de passage. Mais il faut faire attention : en suivant ce principe, sur les premiers temps des deuxième et troisième mesure, nous aurions des accords de septième d'espèce (puisque la septième est majeure). Or, on ne trouve pas, ou alors exceptionnellement, d'accord de septième d'espèce dans le baroque, mais quasi exclusivement des accords de septième de dominante. Donc au début de la deuxième mesure, le ''la''♮ est une appoggiature du ''si''♭, l'accord est donc ''si''♭-''ré''-''fa'', un asscord de quinte. De même, au début de la troisième mesure, le ''sol'' est une appoggiature du ''la''♭. Il faut donc se méfier d'une analyse purement « mathématique ». Il faut s'attacher à ressentir la musique, et à connaître les styles, pour faire une analyse pertinente. Ci-dessous, nous avons grisé les notes étrangères. [[Fichier:Sonate hautbois basson heinichen 2e mvt mes49 analyse.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen. Analyse de la progression harmonique.]] Le chiffrage jazz équivalent est : : | E♭ F– | B♭<sup>Δ</sup> E♭ | A♭<sup>Δ</sup> D– | G … Nous remarquons une progression assez régulière : : ''mi''♭ ↗[2<sup>de</sup>] ''fa'' | ↘[5<sup>te</sup>] ''si''♭ ↗[4<sup>te</sup>] ''mi''♭ | ↘[5<sup>te</sup>] ''la''♭ ↗[4<sup>te</sup>] ''ré'' | ↘[5<sup>te</sup>] ''sol'' Le ''mi''♭ est le degré {{Times New Roman|III}} de la tonalité principale (''do'' mineur), c'est donc une tonique faible ; il « joue le même rôle » qu'un ''do''. S'il y avait eu un accord de ''do'' au début de l'extrait, on aurait eu une progression parfaitement régulière ↗[4<sup>te</sup>] ↘[5<sup>te</sup>]. Nous avons les modulations suivantes : * mesure 49 : ''do'' mineur naturel (le ''si''♭ n'est pas une sensible) avec un accord sur “{{Times New Roman|I}}” (tonique faible, {{Times New Roman|III}}, pour la première analyse, ou bien tonique forte, {{Times New Roman|I}}, pour la seconde) suivi d'un accord sur {{Times New Roman|IV}} ; * mesure 50 : ''si''♭ majeur avec un accord sur {{Times New Roman|I}} suivi d'un accord sur {{Times New Roman|IV}} ; * mesure 51 : ''la''♭ majeur avec un accord sur {{Times New Roman|I}}, et emprunt à ''do'' majeur avec un accord sur {{Times New Roman|II}} ({{Times New Roman|IV}} faible). On a donc une marche harmonique {{Times New Roman|I}} → {{Times New Roman|IV}} qui descend d'une seconde majeure (un ton) à chaque mesure (''do'' → ''si''♭ → ''la''♭), avec une exception sur la dernière mesure (modulation en cours de mesure et descente d'une seconde mineure au lieu de majeure). Ce passage est donc construit sur une régularité, une règle qui crée un effet d'attente — enchaînement {{Times New Roman|I}}<sup>5</sup> → {{Times New Roman|IV}}<sup>5</sup> avec une marche harmonique d'une seconde majeure descendante —, et des « surprises », des exceptions au début — ce n'est pas un accord {{Times New Roman|I}}<sup>5</sup> mais un accord {{Times New Roman|III}}<sup>5</sup> — et à la fin — modulation en milieu de mesure et dernière descente d'une seconde mineure (½t ''la''♭ → ''sol''). L'extrait ne permet pas de le deviner, mais la mesure 52 est un retour en ''do'' mineur, avec donc une modulation sur la dominante (accord de ''sol''<sup>7</sup><sub>+</sub>, G<sup>7</sup>). {{boîte déroulante/fin}} == Progression d'accords == Comme pour la mélodie, la succession des accords dans un morceau, la progression d'accords, suit des règles. Et comme pour la mélodie, les règles diffèrent d'un style musical à l'autre et la créativité consiste à parfois ne pas suivre ces règles. Et comme pour la mélodie, on part d'un ensemble de notes organisé, d'une gamme caractéristique d'une tonalité, d'un mode. Les accords les plus utilisés pour une tonalité donnée sont les accords dont la fondamentale sont les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la tonalité, en particulier la triade {{Times New Roman|I}}, appelée « accord parfait » ou « accord de tonique », et l'accord de septième {{Times New Roman|V}}, appelé « septième de dominante ». Le fait d'avoir une progression d'accords qui se répète permet de structurer un morceau. Pour les morceaux courts, il participe au plaisir de l'écoute et facilite la mémorisation (par exemple le découpage couplet-refrain d'une chanson). Sur les morceaux longs, une trop grande régularité peut introduire de la lassitude, les longs morceaux sont souvent découpés en parties présentant chacune une progression régulière. Le fait d'avoir une progression régulière permet la pratique de l'improvisation : cadence en musique classique, solo en jazz et blues. ; Note : Le terme « cadence » désigne plusieurs choses différentes, et notamment en harmonie : :* une partie improvisée dans un opéra ou un concerto, sens utilisé ci-dessus ; :* une progression d'accords pour ponctuer un morceau et en particulier pour le conclure, sens utilisé dans la section suivante. === Accords peu utilisés === En mode mineur, l'accord de quinte augmentée {{Times New Roman|III<sup>+5</sup>}} est très peu utilisé. C'est un accord dissonant ; il intervient en général comme appogiature de l'accord de tonique (par exemple en ''la'' mineur : {{Times New Roman|III<sup>+5</sup>}} ''do'' - ''mi'' - ''sol''♯ → {{Times New Roman|I<sup>6</sup>}} ''do'' - ''mi'' - ''la''), ou de l'accord de dominante ({{Times New Roman|III<sup>6</sup><sub>+3</sub>}} ''mi'' - ''sol''♯ - ''do'' → {{Times New Roman|V<sup>5</sup>}} ''mi'' - ''sol''♯ - ''si''). Il peut être aussi utilisé comme préparation à l'accord de sous-dominante (enchaînement {{Times New Roman|III}} → {{Times New Roman|IV}}). Par ailleurs, il a une constitution symétrique — c'est l'empilement de deux tierces majeures — et ses renversements ont les mêmes intervalles à l'enharmonie près (quinte augmentée/sixte mineure, tierce majeure/quarte diminuée). De ce fait, un même accord est commun, par renversement et à l'enharmonie près, à trois tonalités : le premier renversement de l'accord ''do'' - ''mi'' - ''sol''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur) est enharmonique à ''mi'' - ''sol''♯ - ''si''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''do''♯ mineur) ; le second renversement est enharmonique à ''la''♭ - ''do'' - ''mi'' ({{Times New Roman|III}}<sup>e</sup> degré de ''fa'' mineur). === Accords très utilisés === Les trois accords les plus utilisés sont les accords de tonique (degré {{Times New Roman|I}}), de sous-dominante ({{Times New Roman|IV}}) et de dominante ({{Times New Roman|V}}). Ils interviennent en particulier en fin de phrase, dans les cadences. L'accord de dominante sert souvent à introduire une modulation : la modulation commence sur l'accord de dominante de la nouvelle tonalité. On note que l'accord de sous-dominante est situé une quinte juste en dessous de la tonique, les accords de dominante et de sous-dominante sont donc symétriques. En jazz, on utilise également très fréquemment l'accord de la sus-tonique (degré {{Times New Roman|II}}), souvent dans des progressions {{Times New Roman|II}} - {{Times New Roman|V}} (- {{Times New Roman|I}}). Rappelons que l'accord de sus-tonique a la fonction de sous-dominante. === Cadences et ''turnaround'' === Le terme « cadence » provient de l'italien ''cadenza'' et désigne la « chute », la fin d'un morceau ou d'une phrase musicale. On distingue deux types de cadences : * les cadences conclusive, qui créent une sensation de complétude ; * les cadences suspensives, qui crèent une sensation d'attente. ==== Cadence parfaite ==== [[Fichier:Au clair de le lune cadence parfaite.midi|thumb|''Au clair de la lune'', harmonisé avec une cadence parfaite (italienne).]] [[Fichier:Au clair de le lune mineur cadence parfaite.midi|thumb|''Idem'' mais en mode mineur harmonique.]] La cadence parfaite est l'enchaînement de l'accord de dominante suivi de l'accord parfait : {{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}}, les deux accord étant à l'état fondamental. Elle donne une impression de stabilité et est donc très souvent utilisée pour conclure un morceau. C'est une cadence conclusive. On peut aussi utiliser l'accord de septième de dominante, la dissonance introduisant une tension résolue par l'accord parfait : {{Times New Roman|V<sup>7</sup><sub>+</sub> - I<sup>5</sup>}}. Elle est souvent précédée de l'accord construit sur le IV<sup>e</sup> degré, appelé « accord de préparation », pour former la cadence italienne : {{Times New Roman|IV<sup>5</sup> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}}. Elle est également souvent précédée du second renversement de l'accord de tonique, qui est alors appelé « appoggiature de la cadence » : {{Times New Roman|I<sup>6</sup><sub>4</sub> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}} (on remarque que les accords {{Times New Roman|I}}<sup>6</sup><sub>4</sub> et {{Times New Roman|V}}<sup>5</sup> ont la basse en commun, et que l'on peut passer de l'un à l'autre par un mouvement conjoint sur les autres notes). {{clear}} ==== Demi-cadence ==== [[Fichier:Au clair de le lune demi cadence.midi|thumb|''Au clair de la lune'', harmonisé avec une demi-cadence.]] Une demi-cadence est une phrase ou un morceau se concluant sur l'accord construit sur le cinquième degré. Il provoque une sensation d'attente, de suspens. Il s'agit en général d'une succession {{Times New Roman|II - V}} ou {{Times New Roman|IV - V}}. C'est une cadence suspensive. On utilise rarement un accord de septième de dominante. {{clear}} ==== Cadence rompue ou évitée ==== La cadence rompue, ou cadence évitée, est succession d'un accord de dominante et d'un accord de sus-dominante, {{Times New Roman|V}} - {{Times New Roman|VI}}. C'est une cadence suspensive. ==== Cadence imparfaite ==== Une cadence imparfaite est une cadence {{Times New Roman|V - I}}, comme la cadence parfaite, mais dont au moins un des deux accords est dans un état renversé. ==== Cadence plagale ==== La cadence plagale — du grec ''plagios'', oblique, en biais — est la succession de l'accord construit sur le quatrième degré, suivi de l'accord parfait : {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}. Elle peut être utilisée après une cadence parfaite ({{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}} - {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}). Elle donne un caractère solennel, voire religieux — elle est parfois appelée « cadence amen » —, elle a un côté antique qui rappelle la musique modale et médiévale<ref>{{lien web |url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/la-cadence-amen-ou-comment-se-dire-adieu-7191921 |titre=La cadence « Amen » ou comment se dire adieu |auteur=Max Dozolme (MAXXI Classique) |site=France Musique |date=2025-04-25 |consulté le=2025-04-25}}.</ref>. C'est une cadence conclusive. ==== {{lang|en|Turnaround}} ==== [[Fichier:Au clair de le lune turnaround.midi|thumb|Au clair de la lune, harmonisé en style jazz : accords de 7{{e}}, anatole suivie d'un ''{{lang|en|turnaround}}'' ii-V-I.]] Le terme ''{{lang|en|turnaround}}'' signifie revirement, retournement. C'est une succession d'accords que fait la transition entre deux parties, en créant une tension-résolution. Le ''{{lang|en|turnaround}}'' le plus courant est la succession {{Times New Roman|II - V - I}}. On utilise également fréquemment l'anatole : {{Times New Roman|I - VI - II - V}}. {| class="wikitable" |+ Progressions typiques d'accords dans une tonalité majeure ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Cadence<br />parfaite<br /> {{Times New Roman|V - I}} ! scope="col" | Cadence<br />italienne<br />{{Times New Roman|IV - V - I}} ! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou IV - V}} ! scope="col" | Cadence<br />plagale<br />{{Times New Roman|IV - I}} ! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}} ! scope="col" | Anatole<br />{{Times New Roman|I - vi - ii - V}} |- |''Do'' majeur || || G - C || F - G - C || Dm - G ou F - G || F - C || Dm - G - C || C - Am - Dm - G |- |''Sol'' majeur || ''fa''♯ || D - G || C - D - G || Am - D ou C - D || C - G || Am - D - G || G - Em - Am - D |- |''Ré'' majeur || ''fa''♯, ''do''♯ || A - D || G - A - D || Em - A ou G - A || G - D || Em - A - D || D - Bm - Em - A |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || E - A || D - E - A || Bm - E ou D - E || D - A || Bm - E - A || A - F♯m - B - E |- | ''Fa'' majeur || ''si''♭ || C - F || B♭ - C - F || Gm - C ou B♭ - C || B♭ - F || Gm - C - F || F - Dm - Gm - C |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || F - B♭ || E♭ - F - B♭ || Cm - F ou E♭ - F || E♭ - B♭ || Cm - F - B♭ || B♭ - Gm - Cm - F |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || B♭ - E♭ || A♭ - B♭ - E♭ || Fm - B♭ ou A♭ - B♭ || A♭ - E♭ || Fm - B♭ - E♭ || Gm - Cm - Fm - B♭ |} {| class="wikitable" |+ Progressions typiques d'accords dans une tonalité mineure ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Cadence<br />parfaite<br />{{Times New Roman|V - i}} ! scope="col" | Cadence<br />italienne<br />{{Times New Roman|iv - V - i}} ! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou iv - V}} ! scope="col" | Cadence<br />plagale<br />{{Times New Roman|iv - i}} ! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}} ! scope="col" | Anatole<br />{{Times New Roman|i - VI - ii - V}} |- | ''La'' mineur<br />harmonique || || E - Am || Dm - E - Am || B° - E ou Dm - E || Dm - Am || B° - E - Am || Am - F - B° - E |- | ''Mi'' mineur<br />harmonique || ''fa''♯ || B - Em || Am - B - Em || F♯° - B ou Am - B || Am - Em || F♯° - B - Em || Em - C - F♯° - B |- | ''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || F♯ - Bm || Em - F♯ - Bm || C♯° - F♯ ou Em - F♯ || Em - Bm || C♯° - F♯ - Bm || Bm - G - C♯° - F♯ |- | ''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || C♯ - F♯m || Bm - C♯ - F♯m || G♯° - C♯ ou Bm - C♯ || Bm - F♯m || G♯° - C♯ - F♯m || A+ - D - G♯° - C♯ |- | ''Ré'' mineur<br />harmonique || ''si''♭ || A - Dm || Gm - A - Dm || E° - A ou Gm - A || Gm - Dm || E° - A - Dm || Dm - B♭ - E° - A |- | ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || D - Gm || Cm - D - Gm || A° - D ou Cm - D || Cm - Gm|| A° - D - Gm || Gm - E♭ - A° - D |- | ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || G - Cm || Fm - G - Cm || D° - G ou Fm - G || Fm - Dm || D° - G - Cm || Cm - A♭ - D° - G |} ==== Exemple : ''La Mer'' ==== : {{lien web | url = https://www.youtube.com/watch?v=PXQh9jTwwoA | titre = Charles Trenet - La mer (Officiel) [Live Version] | site = YouTube | auteur = Charles Trenet | consulté le = 2020-12-24 }} Le début de ''La Mer'' (Charles Trenet, 1946) est en ''do'' majeur et est harmonisé par l'anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (C - Am - Dm - G<sup>7</sup>) sur deux mesures, jouée deux fois ({{Times New Roman|1=<nowiki>|I-vi|ii-V</nowiki><sup>7</sup><nowiki>|</nowiki>}} × 2). Viennent des variations avec les progressions {{Times New Roman|I-III-vi-V<sup>7</sup>}} (C - E - Am - G<sup>7</sup>) puis la « progression ’50s » (voir plus bas) {{Times New Roman|I-vi-IV-VI<sup>7</sup>}} (C - Am - F - A<sup>7</sup>, on remarque que {{Times New Roman|IV}}/F est le relatif majeur du {{Times New Roman|ii}}/Dm de l'anatole), jouées chacune une fois sur deux mesure ; puis cette première partie se conclut par une demie cadence {{Times New Roman|ii-V<sup>7</sup>}} sur une mesure puis une dernière anatole sur trois mesures ({{Times New Roman|1=<nowiki>|I-vi|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}}). Cela constitue une première partie « A » sur douze mesures qui se termine par une demi-cadence ({{Times New Roman|ii-V<sup>7</sup>}}) qui appelle une suite. Cette partie A est jouée une deuxième fois mais la fin est modifiée pour la transition : les deux dernières mesures {{Times New Roman|<nowiki>|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}} deviennent {{Times New Roman|<nowiki>|ii-V</nowiki><sup>7</sup><nowiki>|I|</nowiki>}} (|Dm-G7|C|), cette partie « A’ » se conclut donc par une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}). Le morceau passe ensuite en tonalité de ''mi'' majeur, donc une tierce au dessus de ''do'' majeur, sur six mesures. Cette partie utilise une progression ’50s {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (E - C♯m - A - B<sup>7</sup>), qui est rappelons-le une variation de l'anatole, l'accord {{Times New Roman|ii}} (Fm) étant remplacé par son relatif majeur {{Times New Roman|IV}} (A). Cette anatole modifiée est jouée deux fois puis la partie en ''mi'' majeur se conclut par l'accord parfait {{Times New Roman|I}} joué sur deux mesures (|E|E|), on a donc, avec la mesure précédente, avec une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}). Suivent ensuite six mesures en ''sol'' majeur, donc à nouveau une tierce au dessus de ''mi'' majeur. Elle comporte une progression {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (G - Em - C - D<sup>7</sup>), donc anatole avec substitution du {{Times New Roman|ii}}/Am par son relatif majeur {{Times New Roman|VI}}/C (progression ’50s), puis une anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (G - Em - Am - D<sup>7</sup>) et deux mesure sur la tonique {{Times New Roman|I-I<sup>7</sup>}} (G - G<sup>7</sup>), formant à nouveau une cadence parfaite. La fin sur un accord de septième, dissonant, appelle une suite. Cette partie « B » de douze mesures comporte donc deux parties similaires « B1 » et « B2 » qui forment une marche harmonique (montée d'une tierce). Le morceau se conclut par une reprise de la partie « A’ » et se termine donc par une cadence parfaite. Nous avons une structure A-A’-B-A’ sur 48 mesures, proche la forme AABA étudiée plus loin. Donc ''La Mer'' est un morceau structuré autour de l'anatole avec des variations (progression ’50s, substitution du {{Times New Roman|ii}} par son relatif majeur {{Times New Roman|IV}}) et comportant une marche harmonique dans sa troisième partie. Les parties se concluent par des ''{{lang|en|turnarounds}}'' sous la forme d'une cadence parfaite ou, pour la partie A, par une demi-cadence. {| border="1" rules="rows" frame="hsides" |+ Structure de ''La Mer'' |- align="center" | | colspan="12" | ''do'' majeur | |- align="center" ! scope="row" rowspan=2 | A | colspan="2" | anatole | colspan="2" | // | colspan="2" | variation | colspan="2" | ’50s | ½ c. | colspan="3" | anatole | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii}} || <nowiki>|</nowiki> {{Times New Roman|V<sup>7</sup>}} || <nowiki>|</nowiki> |- align="center" ! scope="row" rowspan="2" | A’ | colspan="2" | anatole | colspan="2" | // | colspan="2" | variation | colspan="2" | ’50s | ½ c. | colspan="2" | anatole | c.p. | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> |- align="center" | | colspan="6" | B1 : ''mi'' majeur | colspan="6" background="lightgray" | B2 : ''sol'' majeur | |- align="center" ! scope="row" rowspan="2" | B | colspan="2" | ’50s | colspan="2" | // |colspan="2" | c.p. | colspan="2" | ’50s | colspan="2" | // |colspan="2" | c.p. | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I<sup>7</sup>}} || <nowiki>|</nowiki> |- ! scope="row" | A’ | colspan="12" | | |} === Progression blues === La musique blues est apparue dans les années 1860. Elle est en général bâtie sur une grille d'accords ''({{lang|en|changes}})'' immuable de douze mesures ''({{lang|en|twelve-bar blues}})''. C'est sur cet accompagnement qui se répète que s'ajoute la mélodie — chant et solo. Cette structure est typique du blues et se retrouve dans ses dérivés comme le rock 'n' roll. Le rythme est toujours un rythme ternaire syncopé ''({{lang|en|shuffle, swing, groove}}, ''notes inégales'')'' : la mesure est à quatre temps, mais la noire est divisée en noire-croche en triolet, ou encore triolet de croche en appuyant la première et la troisième. La mélodie se construit en général sur une gamme blues de six degrés (gamme pentatonique mineure avec une quarte augmentée), mais bien que la gamme soit mineure, l'harmonie est construite sur la gamme majeure homonyme : un blues en ''fa'' a une mélodie sur la gamme de ''fa'' mineur, mais une harmonie sur la gamme de ''fa'' majeur. La grille d'accord comporte les accords construits sur les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la gamme majeure homonyme. Les accords sont souvent des accords de septième (donc avec une tierce majeure et une septième mineure), il ne s'agit donc pas d'une harmonisation de gamme diatonique (puisque la septième est majeure sur l'accord de tonique). Par exemple, pour un blues en ''do'' : * accord parfait de do majeur, C ({{Times New Roman|I}}<sup>er</sup> degré) ; * accord parfait de fa majeur, F ({{Times New Roman|IV}}<sup>e</sup> degré) ; * accord parfait de sol majeur, G ({{Times New Roman|V}}<sup>e</sup> degré). Il existe quelques morceaux harmonisés avec des accords mineurs, comme par exemple ''As the Years Go Passing By'' d'Albert King (Duje Records, 1959). La progression blues est organisée en trois blocs de quatre mesures ayant les fonctions suivantes (voir ci-dessus ''[[#Harmonie fonctionnelle|Harmonie fonctionnelle]]'') : * quatre mesures toniques ; * quatre mesures sous-dominantes ; * quatre mesures dominantes. La forme la plus simple, que Jeff Gardner appelle « forme A », est la suivante : {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, forme A |- ! scope="row" | Tonique | width="50px" | I | width="50px" | I | width="50px" | I | width="50px" | I |- ! scope="row" | Sous-domminante | width="50px" | IV | width="50px" | IV | width="50px" | I | width="50px" | I |- ! scope="row" | Dominante | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | V |} La progression {{Times New Roman|I-V}} des deux dernières mesures forment le ''{{lang|en|turnaround}}'', la demie cadence qui lance le cycle suivant. Nous présentons ci-dessous un exemple typique de ligne de basse ''({{lang|en|walking bass}})'' pour le ''{{lang|en|turnaround}}'' d'un blues en ''la'' : [[Fichier:Turnaround classique blues en la.svg|Exemple typique de ligne de basse pour un ''turnaround'' de blues en ''la''.]] [[Fichier:Blues mi harmonie elementaire.midi|thumb|Blues en ''mi'', harmonisé de manière élémentaire avec une ''{{lang|en|walking bass}}''.]] Vous pouvez écouter ci-contre une harmonisation typique d'un blues en ''mi''. Les accords sont exécutés par une basse marchante ''({{lang|en|walking bass}})'', qui joue une arpège sur la triade avec l'ajout d'une sixte majeure et d'une septième mineure, et par une guitare qui joue un accord de puissance ''({{lang|en|power chord}})'', qui n'est composé que de la fondamentale et de la quinte juste, avec une sixte en appoggiature. La forme B s'obtient en changeant la deuxième mesure : on joue un degré {{Times New Roman|IV}} au lieu d'un degré {{Times New Roman|I}}. La progression {{Times New Roman|I-IV}} sur les deux premières mesures est appelé ''{{lang|en|quick change}}''. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, forme B |- | width="50px" | I | width="50px" | IV | width="50px" | I | width="50px" | I |- | width="50px" | IV | width="50px" | IV | width="50px" | I | width="50px" | I |- | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | V |} Par exemple, ''Sweet Home Chicago'' (Robert Johnson, 1936) est un blues en ''fa'' ; sa grille d'accords, aux variations près, suit une forme B : {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression de ''Sweet Home Chicago'' |- | width="50px" | F | width="50px" | B♭ | width="50px" | F | width="50px" | F |- | width="50px" | B♭ | width="50px" | B♭ | width="50px" | F | width="50px" | F |- | width="50px" | C7 | width="50px" | B♭7 | width="50px" | F7 | width="50px" | C7 |} : Écouter {{lien web | url =https://www.youtube.com/watch?v=dkftesK2dck | titre = Robert Johnson "Sweet Home Chicago" | auteur = Michal Angel | site = YouTube | date = 2007-12-09 | consulté le = 2020-12-17 }} Les formes C et D s'obtiennent à partir des formes A et B en changeant le dernier accord par un accord sur le degré {{Times New Roman|I}}, ce qui forme une cadence plagale. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, formes C et D |- | colspan="4" | … |- | colspan="4" | … |- | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | I |} L'harmonie peut être enrichie, notamment en jazz. Voici par exemple une grille du blues souvent utilisés en bebop. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Exemple de progression de blues bebop sur une base de forme B |- | width="60px" | I<sup>7</sup> | width="60px" | IV<sup>7</sup> | width="60px" | I<sup>7</sup> | width="60px" | V–<sup>7</sup> <nowiki>|</nowiki> I<sup>7</sup> |- | width="60px" | IV<sup>7</sup> | width="60px" | IV<sup>7</sup> | width="60px" | I<sup>7</sup> | width="60px" | VI<sup>7 ♯9 ♭13</sup> |- | width="60px" | II–<sup>7</sup> | width="60px" | V<sup>7</sup> | width="60px" | V<sup>7</sup> <nowiki>|</nowiki> IV<sup>7</sup> | width="60px" | II–<sup>7</sup> <nowiki>|</nowiki> V<sup>7</sup> |} On peut aussi trouver des blues sur huit mesures, sur seize mesures comme ''Watermelon Man'' de Herbie Hancock (album ''Takin' Off'', Blue Note, 1962) ou ''Let's Dance'' de Jim Lee (interprété par Chris Montez, Monogram, 1962) * {{lien web |url= https://www.dailymotion.com/video/x5iduwo |titre=Herbie Hancock - Watermelon Man (1962) |auteur=theUnforgettablesTv |site=Dailymotion |date=2003 |consulté le=2021-02-09 }} * {{lien web |url=https://www.youtube.com/watch?v=6JXshurYONc |titre=Let's Dance |auteur=Chris Montez |site=YouTube |date=2016-08-06 |consulté le=2021-02-09 }} À l'inverse, certains blues peuvent avoir une structure plus simple que les douze mesure ; par exemple ''Hoochie Coochie Man'' de Willie Dixon (interprété par Muddy Waters sous le titre ''Mannish Boy'', Chicago Blues, 1954) est construit sur un seul accord répété tout le long de la chanson. * {{lien web |url=https://www.dailymotion.com/video/x5iduwo |titre=Muddy Waters - Hoochie Coochie Man |auteur=Muddy Waters |site=Dailymotion |date=2012 | consulté le=2021-02-09 }} === Cadence andalouse === La cadence andalouse est une progression de quatre accords, descendant par mouvement conjoint : * en mode de ''mi'' (mode phrygien) : {{Times New Roman|IV}} - {{Times New Roman|III}} - {{Times New Roman|II}} - {{Times New Roman|I}} ;<br />par exemple en ''mi'' phrygien : Am - G - F - E ; en ''do'' phrygien : Fm - E♭ - D♭ - C ;<br />on notera que le degré {{Times New Roman|III}} est diésé dans l'accord final (ou bécarre s'il est bémol dans la tonalité) ; * en mode mineur : {{Times New Roman|I}} - {{Times New Roman|VII}} - {{Times New Roman|VI}} - {{Times New Roman|V}} ;<br />par exemple en ''la'' mineur : Am - G - F - E ; en ''do'' mineur : Cm - B♭ - A♭ - m ;<br />comme précédemment, on notera que le degré {{Times New Roman|VII}} est diésé dans l'accord final. === Progressions selon le cercle des quintes === [[Fichier:Cercle quintes degres tonalite majeure.svg|vignette|Cercle des quinte justes (parcouru dans le sens des aiguilles d'une montre) des degrés d'une tonalité majeure.]] La progression {{Times New Roman|V-I}} est la cadence parfaite, mais on peut aussi l'employer au milieu d'un morceau. Cette progression étant courte, sa répétition crée de la lassitude ; on peut la compléter par d'autres accords séparés d'une quinte juste, en suivant le « cercle des quintes » : {{Times New Roman|I-V-IX}}, la neuvième étant enharmonique de la seconde, on obtient {{Times New Roman|I-V-II}} ; en retournant la progression, on obtient le turnaround {{Times New Roman|II-V-I}}. En jazz, dans le morceau ''Topsy'' (Edgar Battle et Eddir Durham), la partie <span style="border-color: black; border-style: solid; border-width: thin;">C</span> est composée d'une progression en quintes descendantes d'accords de septième de dominante : D<sup>7</sup> - G<sup>7</sup> - C<sup>7</sup> - F<sup>7</sup> - B♭<sup>7</sup> ; les improvisatiosn sur cette partie peuvent donc se faire en modulant en ''sol'' (majeur ou mineur) - ''do'' (M/m) - ''fa'' (M/m) - ''si''♭ (M/m) - ''mi''♭ (M/m). On peut continuer de décrire le cercle des quintes : {{Times New Roman|I-V-II-VI}}, on obtient l'anatole dans le désordre ; on peut à l'inverse étendre les quintes vers la gauche, {{Times New Roman|IV-I-V-II-VI}}. En musique populaire, on trouve fréquemment une progression fondée sur les accord {{Times New Roman|I}}, {{Times New Roman|IV}}, {{Times New Roman|V}} et {{Times New Roman|VI}}, popularisée dans les années 1950. La « progression années 1950 », « progression ''{{lang|en|fifties ('50)}}'' » ''({{lang|en|'50s progression}})'' est dans l'ordre {{Times New Roman|I-VI-IV-V}}. On trouve aussi cette progression en musique classique. Si la tonalité est majeure, la triade sur la sus-dominante est mineure, les autres sont majeures, on notera donc souvent {{Times New Roman|I-vi-IV-V}}. On peut avoir des permutations circulaires (le dernier accord venant au début, ou vice-versa) : {{Times New Roman|vi-IV-V-I}}, {{Times New Roman|IV-V-I-vi}} et {{Times New Roman|V-I-vi-IV}}. {| class="wikitable" |+ Accords selon la tonalité ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" style="font-family:Times New Roman" | I ! scope="col" style="font-family:Times New Roman" | IV ! scope="col" style="font-family:Times New Roman" | V ! scope="col" style="font-family:Times New Roman" | vi |- |''Do'' majeur || || C || F || G || Am |- |''Sol'' majeur || ''fa''♯ || G || C || D || Em |- |''Ré'' majeur || ''fa''♯, ''do''♯ || D || G || A || Bm |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A || D || E || F♯m |- | ''Fa'' majeur || ''si''♭ || F || B♭ || C || Dm |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭ || E♭ || F || Gm |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭ || A♭ || B♭ || Cm |} Par exemple, en tonalité de ''do'' majeur, la progression {{Times New Roman|I-vi-IV-V}} sera C-Am-F-G. Il existe d'autres progressions utilisant ces accords mais dans un autre ordre, typiquement {{Times New Roman|I–IV–vi–V}} ou une de ses permutations circulaires : {{Times New Roman|IV–vi–V-I}}, {{Times New Roman|vi–V-I-IV}} ou {{Times New Roman|V-I-IV-vi}}. Ou dans un autre ordre. PV Nova l'illustre dans plusieurs de ses « expériences » dans la version {{Times New Roman|vi-V-IV-I}}, soit Am-G-F-C, ou encore {{Times New Roman|vi-IV-I-V}}, soit Am-F-C-G : : {{lien web | url = https://www.youtube.com/watch?v=w08LeZGbXq4 | titre = Expérience n<sup>o</sup> 6 — La Happy Pop | auteur = PV Nova | site = YouTube | date = 2011-08-20 | consulté le = 2020-12-13 }} et cela devient un gag récurrent avec son « chapeau des accords magiques qu'on nous ressort à toutes les sauces » : {{lien web | url = https://www.youtube.com/watch?v=VMY_vc4nZAU | titre = Expérience n<sup>o</sup> 14 — La Soupe dou Brasil | auteur = PV Nova | site = YouTube | date = 2012-10-03 | consulté le = 2020-12-17 }} Cette récurrence est également parodiée par le groupe The Axis of Awesome avec ses « chansons à quatre accords » ''({{lang|en|four-chords song}})'', dans une sketch où ils mêlent 47 chansons en utilisant l'ordre {{Times New Roman|I-V-vi-IV}} : : {{lien web | url = https://www.youtube.com/watch?v=oOlDewpCfZQ | titre = 4 Chords | Music Videos | The Axis Of Awesome | auteur = The Axis of Awesome | site = YouTube | date = 2011-07-20 | consulté le = 2020-12-17 }} {{boîte déroulante/début|titre=Chansons mêlées dans le sketch}} # Journey : ''Don't Stop Believing'' ; # James Blunt : ''You're Beautiful'' ; # Black Eyed Peas : ''Where Is the Love'' ; # Alphaville : ''Forever Young'' ; # Jason Mraz : ''I'm Yours'' ; # Train : ''Hey Soul Sister'' ; # The Calling : ''Wherever You Will Go'' ; # Elton John : ''Can You Feel The Love Tonight'' (''Le Roi lion'') ; # Akon : ''Don't Matter'' ; # John Denver : ''Take Me Home, Country Roads'' ; # Lady Gaga : ''Paparazzi'' ; # U2 : ''With Or Without You'' ; # The Last Goodnight : ''Pictures of You'' ; # Maroon Five : ''She Will Be Loved'' ; # The Beatles : ''Let It Be'' ; # Bob Marley : ''No Woman No Cry'' ; # Marcy Playground : ''Sex and Candy'' ; # Men At Work : ''Land Down Under'' ; # thème de ''America's Funniest Home Videos'' (équivalent des émissions ''Vidéo Gag'' et ''Drôle de vidéo'') ; # Jack Johnson : ''Taylor'' ; # Spice Girls : ''Two Become One'' ; # A Ha : ''Take On Me'' ; # Green Day : ''When I Come Around'' ; # Eagle Eye Cherry : ''Save Tonight'' ; # Toto : ''Africa'' ; # Beyonce : ''If I Were A Boy'' ; # Kelly Clarkson : ''Behind These Hazel Eyes'' ; # Jason DeRulo : ''In My Head'' ; # The Smashing Pumpkins : ''Bullet With Butterfly Wings'' ; # Joan Osborne : ''One Of Us'' ; # Avril Lavigne : ''Complicated'' ; # The Offspring : ''Self Esteem'' ; # The Offspring : ''You're Gonna Go Far Kid'' ; # Akon : ''Beautiful'' ; # Timberland featuring OneRepublic : ''Apologize'' ; # Eminem featuring Rihanna : ''Love the Way You Lie'' ; # Bon Jovi : ''It's My Life'' ; # Lady Gaga : ''Pokerface'' ; # Aqua : ''Barbie Girl'' ; # Red Hot Chili Peppers : ''Otherside'' ; # The Gregory Brothers : ''Double Rainbow'' ; # MGMT : ''Kids'' ; # Andrea Bocelli : ''Time To Say Goodbye'' ; # Robert Burns : ''Auld Lang Syne'' ; # Five for fighting : ''Superman'' ; # The Axis of Awesome : ''Birdplane'' ; # Missy Higgins : ''Scar''. {{boîte déroulante/fin}} Vous pouvez par exemple jouer les accords C-G-Am-F ({{Times New Roman|I-V-vi-IV}}) et chanter dessus ''{{lang|en|Let It Be}}'' (Paul McCartney, The Beattles, 1970) ou ''Libérée, délivrée'' (Robert Lopez, ''La Reine des neiges'', 2013). La progression {{Times New Roman|I-V-vi-IV}} est considérée comme « optimiste » tandis que sa variante {{Times New Roman|iv-IV-I-V}} est considérée comme « pessimiste ». On peut voir la progression {{Times New Roman|I-vi-IV-V}} comme une variante de l'anatole {{Times New Roman|I-vi-ii-V}}, obtenue en remplaçant l'accord de sustonique {{Times New Roman|ii}} par l'accord de sous-dominante {{Times New Roman|IV}} (son relatif majeur, et degré ayant la même fonction). ==== Exemples de progression selon le cercle des quintes en musique classique ==== [[Fichier:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.midi|vignette|Dietrich Buxtehude, Psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]] Cette progression selon la cercle des quintes, sous la forme {{Times New Roman|I-vi-IV-V}}, apparaît déjà au {{pc|xvii}}<sup>e</sup> siècle dans le psaume 42 ''Quem ad modum desiderat cervis'' (BuxVW92) de Dietrich Buxtehude (1637-1707). Le morceau est en ''fa'' majeur, la progression d'accords est donc F-Dm-B♭-C. : {{lien web | url = https://www.youtube.com/watch?v=8FmV9l1RqSg | titre = D. Buxtehude - Quemadmodum desiderat cervus, BuxWV 92 | auteur = Longobardo | site = YouTube | date = 2013-04-06 | consulté la = 2021-01-01 }} [[File:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.svg|vignette|450x450px|center|Dietrich Buxtehude, psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]] {{clear}} [[Fichier:JSBach BWV140 cantate 4 mesures.midi|vignette|J.-S. Bach, cantate BWV140, quatre premières mesures.]] On la trouve également dans l'ouverture de la cantate ''{{lang|de|Wachet auf, ruft uns die Stimme}}'' de Jean-Sébastien Bach (BWV140, 1731). Le morceau est en ''mi''♭ majeur, la progression d'accords est donc E♭-Cm-A♭<sup>6</sup>-B♭. [[Fichier:JSBach BWV140 cantate 4 mesures.svg|vignette|center|J.-S. Bach, cantate BWV140, quatre premières mesures.|alt=|517x517px]] {{clear}} [[Fichier:Mozart K310 Sonate8 mesures 45 a 49.midi|vignette|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]] La même progression est utilisée par Mozart, par exemple dans le premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778), la progression d'accords est C-Am-F-G qui correspond à la progression {{Times New Roman|III-i-VI-VII}} de ''la'' mineur, mais à la progression {{Times New Roman|I-vi-IV-V}} de la gamme relative, ''do'' majeur . [[Fichier:Mozart K310 Sonate8 mesures 45 a 49.svg|vignette|center|500px|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]] === Substitution tritonique === Un des accords les plus utilisés est donc l'accord de septième de dominante, {{Times New Roman|V<sup>7</sup><sub>+</sub>}} qui contient les degrés {{Times New Roman|V}}, {{Times New Roman|VII}}, {{Times New Roman|II}} ({{Times New Roman|IX}}) et {{Times New Roman|IV}}({{Times New Roman|XI}}) ; par exemple, en tonalité de ''do'' majeur, l'accord de ''sol'' septième (G<sup>7</sup>) contient les notes ''sol''-''si''-''ré''-''fa''. Si l'on prend l'accord dont la fondamentale est trois tons (triton) au-dessus ou en dessous — l'octave contenant six tons, on arrive sur la même note —, {{Times New Roman|♭II<sup>7</sup>}}, ici ''ré''♭ septième (D♭<sup>7</sup>), celui-ci contient les notes ''ré''♭-''fa''-''la''♭-''do''♭, cette dernière note étant l'enharmonique de ''si''. Les deux accords G<sup>7</sup> et D♭<sup>7</sup> ont donc deux notes en commun : le ''fa'' et le ''si''/''do''♭. Il est donc fréquent en jazz de substituer l'accord {{Times New Roman|V<sup>7</sup><sub>+</sub>}} par l'accord {{Times New Roman|♭II<sup>7</sup>}}. Par exemple, la progression {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|V<sup>7</sup>}}-{{Times New Roman|I<sup>&Delta;</sup>}} devient {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|♭II<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}}. C'est un procédé courant de réharmonisation (le fait de remplacer un accord par un autre dans un morceau existant). Les six substitutions possibles sont donc : C<sup>7</sup>↔F♯<sup>7</sup> - D♭<sup>7</sup>↔G<sup>7</sup> - D<sup>7</sup>↔A♭<sup>7</sup> - E♭<sup>7</sup>↔A<sup>7</sup> - E<sup>7</sup>↔B♭<sup>7</sup> - F<sup>7</sup>↔B<sup>7</sup>. [[Fichier:Übermäsiger Terzquartakkord.jpg|vignette|Exemple de cadence parfaite en ''do'' majeur avec substitution tritonique (sixte française).]] Dans l'accord D♭<sup>7</sup>, si l'on remplace le ''do''♭ par son ''si'' enharmonique, on obtient un accord de sixte augmentée : ''ré''♭-''fa''-''la''♭-''si''. Cet accord est utilisé en musique classique depuis la Renaissance ; on distingue en fait trois accords de sixte augmentée : * sixte française ''ré''♭-''fa''-''sol''-''si'' ; * sixte allemande : ''ré''♭-''fa''-''la''♭-''si'' ; * sixte italienne : ''ré''♭-''fa''-''si''. Par exemple, le ''Quintuor en ''ut'' majeur'' de Franz Schubert (1828) se termine par une cadence parfaite dont l'accord de dominante est remplacé par une sixte française ''ré''♭-''fa''-''si''-''sol''-''si'' (''ré''♭ aux violoncelles, ''fa'' à l'alto, ''si''-''sol'' aux seconds violons et ''si'' au premier violon). [[Fichier:Schubert C major Quintet ending.wav|vignette|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]] [[Fichier:Schubert C major Quintet ending.png|vignette|center|upright=2.5|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]] === Autres accords de substitution === Substituer un accord consiste à utiliser un accord provenant d'une tonalité étrangère à la tonalité en cours. À la différence d'une modulation, la substitution est très courte et ne donne pas l'impression de changer de tonalité ; on a juste un sentiment « étrange » passager. Un court passage dans une autre tonalité est également appelée « emprunt ». Nous avons déjà vu plusieurs méthodes de substitution : * utilisation d'une note étrangère : une note étrangère — note de passage, appoggiature, anticipation, retard… — crée momentanément un accord hors tonalité ; en musique classique, ceci n'est pas considéré comme un accord en propre, mais en jazz, on parle « d'accord de passage » et « d'accord suspendu » ; * utilisation d'une dominante secondaire : l'accord de dominante secondaire est hors tonalité ; le but ici est de faire une cadence parfaite, mais sur un autre degré que la tonique de la tonalité en cours ; * la substitution tritonique, vue ci-dessus, pour remplacer un accord de septième de dominante. Une dernière méthode consiste à remplacer un accord par un accord d'une gamme de même tonique, mais d'un autre mode ; on « emprunte » ''({{lang|en|borrow}})'' l'accord d'un autre mode. Par exemple, substituer un accord de la tonalité de ''do'' majeur par un accord de la tonalité de ''do'' mineur ou de ''do'' mode de ''mi'' (phrygien). Donc en ''do'' majeur, on peut remplacer un accord de ''ré'' mineur septième (D<sub>m</sub><sup>7</sup>) par un accord de ''ré'' demi-diminué (D<sup>⌀</sup>, D<sub>m</sub><sup>7&flat;5</sup>) qui est un accord appartenant à la donalité de ''la'' mineur harmonique. === Forme AABA === La forme AABA est composée de deux progressions de huit mesures, notées A et B ; cela représente trente-deux mesures au total, on parle donc souvent en anglais de la ''{{lang|en|32-bars form}}''. C'est une forme que l'on retrouve dans de nombreuses chanson de comédies musicales de Broadway comme ''Have You Met Miss Jones'' (''{{lang|en|I'd Rather Be Right}}'', 1937), ''{{lang|en|Over the Rainbow}}'' (''Le Magicien d'Oz'', Harold Harlen, 1939), ''{{lang|en|All the Things You Are}}'' (''{{lang|en|Very Warm for may}}'', 1939). Par exemple, la version de ''{{lang|en|Over the Rainbow}}'' chantée par Judy Garland est en ''la''♭ majeur et la progression d'accords est globalement : * A (couplet) : A♭-Fm | Cm-A♭ | D♭ | Cm-A♭ | D♭ | D♭-F | B♭-E♭ | A♭ * B (pont) : A♭ | B♭m | Cm | D♭ | A♭ | B♭-G | Cm-G | B♭m-E♭ soit en degrés : * A : {{Times New Roman|<nowiki>I-vi | iii-I | IV | iii-IV | IV | IV-vi | II-V | I</nowiki>}} * B : {{Times New Roman|<nowiki>I | ii | iii | IV | I | II-VII | iii-VII | ii-V</nowiki>}} Par rapport aux paroles de la chanson, on a * A : couplet 1 ''« {{lang|en|Somewhere […] lullaby}} »'' ; * A : couplet 2 ''« {{lang|en|Somewhere […] really do come true}} »'' ; * B : pont ''« {{lang|en|Someday […] you'll find me}} »'' ; * A : couplet 3 ''« {{lang|en|Somewhere […] oh why can't I?}} »'' ; : {{lien web | url = https://www.youtube.com/watch?v=1HRa4X07jdE | titre = Judy Garland - Over The Rainbow (Subtitles) | site = YouTube | auteur = Overtherainbow | consulté le = 2020-12-17 }} Une mise en œuvre de la forme AABA couramment utilisée en jazz est la forme anatole (à le pas confondre avec la succession d'accords du même nom), en anglais ''{{lang|en|rythm changes}}'' car elle s'inspire du morceau ''{{lang|en|I Got the Rythm}}'' de George Gerschwin (''Girl Crazy'', 1930) : * A : {{Times New Roman|I–vi–ii–V}} (succession d'accords « anatole ») ; * B : {{Times New Roman|III<sup>7</sup>–VI<sup>7</sup>–II<sup>7</sup>–V<sup>7</sup>}} (les fondamentales forment une succession de quartes, donc parcourent le « cercle des quintes » à l'envers). Par exemple, ''I Got the Rythm'' étant en ''ré''♭ majeur, la forme est : * A : D♭ - B♭m - E♭m - A♭ * B : F7 - B♭7 - E♭7 - A♭7 === Exemples === ==== Début du Largo de la symphonie du Nouveau Monde ==== [[File:Largo nouveau monde 5 1res mesures.svg|vignette|Partition avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]] [[File:Largo nouveau monde 5 1res mesures.midi|vignette|Fichier son avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]] Nous avons reproduit ci-contre les cinq premières mesure du deuxième mouvement Largo de la symphonie « Du Nouveau Monde » (symphonie n<sup>o</sup> 9 d'Antonín Dvořák, 1893). Cliquez sur l'image pour l'agrandir. Vous pouvez écouter cette partie jouée par un orchestre symphonique : * {{lien web |url =https://www.youtube.com/watch?v=y2Nw9r-F_yQ?t=565 |titre = Dvorak Symphony No.9 "From the New World" Karajan 1966 |site=YouTube (Seokjin Yoon) |consulté le=2020-12-11 }} (à 9 min 25), par le Berliner Philharmoniker, dirigé par Herbert von Karajan (1966) ; * {{lien web |url = https://www.youtube.com/watch?v=ASlch7R1Zvo |titre=Dvořák: Symphony №9, "From The New World" - II - Largo |site=YouTube (diesillamusicae) |consulté le=2020-12-11 }} : Wiener Philharmoniker, dirigé par Herbert von Karajan (1985). {{clear}} Cette partie fait intervenir onze instruments monodiques (ne jouant qu'une note à la fois) : des vents (trois bois, sept cuivres) et une percussion. Certains de ces instruments sont transpositeurs (les notes sur la partition ne sont pas les notes entendues). Jouées ensemble, ces onze lignes mélodiques forment des accords. Pour étudier cette partition, nous réécrivons les parties des instruments transpositeurs en ''do'' et les parties en clef d’''ut'' en clef de ''fa''. Nous regroupons les parties en clef de ''fa'' d'un côté et les parties en clef de ''sol'' d'un autre. {{boîte déroulante|Résultat|contenu=[[File:Largo nouveau monde 5 1res mesures transpositeurs en do.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde, en do.]]}} Nous pouvons alors tout regrouper sous la forme d'un système de deux portées clef de ''fa'' et clef de ''sol'', comme une partition de piano. {{boîte déroulante|Résultat|contenu= [[Fichier:Largo nouveau monde 5 1res mesures accords.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords.]] }} [[Fichier:Largo nouveau monde 5 1res mesures accords.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords.]] {{clear}} Ensuite, nous ne gardons que la basse et les notes médium. Nous changeons éventuellement certaines notes d'octave afin de n'avoir que des superpositions de tierce ou de quinte (état fondamental des accords, en faisant ressortir les notes manquantes). {{boîte déroulante|Résultat|contenu= [[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords simplifiés.]] }} [[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords simplifiés.]] Vous pouvez écouter cette partie jouée par un quintuor de cuivres (trompette, bugle, cor, trombone, tuba), donc avec des accords de cinq notes : : {{lien web |url=https://www.youtube.com/watch?v=pWfe60nbvjA |titre = Largo from The New World Symphony by Dvorak |site=YouTube (The Chamberlain Brass) |consulté le=2020-12-11 }} : The American Academy of Arts & Letters in New York City (2017). Nous allons maintenant chiffrer les accords. Pour établir la basse chiffrée, il nous faut déterminer le parcours harmonique. Pour le premier accord, les tonalités les plus simples avec un ''sol'' dièse sont ''la'' majeur et ''fa'' dièse mineur ; comme le ''mi'' est bécarre, nous retenons ''la'' majeur, il s'agit donc d'un accord de quinte sur la dominante (les accords de dominante étant très utilisés, cela nous conforte dans notre choix). Puis nous avons un ''si'' bémol, nous pouvons être en ''fa'' majeur ou en ''ré'' mineur ; nous retenons ''fa'' majeur, c'est donc le renversement d'un accord sur le degré {{Times New Roman|II}}. Dans la deuxième mesure, nous revenons en ''la'' majeur, puis, avec un ''la'' et un ''ré'' bémols, nous sommes en ''la'' bémol majeur ; nous avons donc un accord de neuvième incomplet sur la sensible, ou un accord de onzième incomplet sur la dominante. Dans la troisième mesure, nous passons en ''ré'' majeur, avec un accord de dominante. Puis, nous arrivons dans la tonalité principale, avec le renversement d'un accord de dominante sans tierce suivi d'un accord de tonique. Nous avons donc une cadence parfaite, conclusion logique d'une phrase. La progression des accords est donc : {| class="wikitable" ! scope="row" | Tonalité | ''la'' M - ''fa'' M || ''la'' M - ''la''♭ M || ''ré'' M - ''ré''♭ M || ''ré''♭ M |- ! scope="row" | Accords | {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|II}}<sup>6</sup><sub>4</sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|“V”}}<sup>9</sup><sub><s>5</s></sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|V}}<sup>+4</sup> || {{Times New Roman|I}}<sup>5</sup> |} Dans le chiffrage jazz, nous avons donc : * une triade de ''mi'' majeur, E ; * une triade de ''sol'' majeur avec un ''ré'' en basse : G/D ; * à nouveau un E ; * un accord de ''sol'' neuvième diminué incomplet, avec un ''ré'' bémol en basse : G dim<sup>9</sup>/D♭ ; * un accord de ''la'' majeur, A ; * un accord de ''la'' bémol septième avec une ''sol'' bémol à la basse : A♭<sup>7</sup>/G♭ ; * la partie se conclue par un accord parfait de ''ré''♭ majeur, D♭. Soit une progression E - G/D | E - G dim<sup>9</sup>/D♭ | A - A♭<sup>7</sup>/G♭ | D♭. [[Fichier:Largo nouveau monde 5 1res mesures accords chiffres.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde en accords simplifiés.]] {{clear}} ==== Thème de Smoke on the Water ==== Le morceau ''Smoke on the Water'' du groupe Deep Purple (album ''Machine Head'', 1972) possède un célèbre thème, un riff ''({{lang|en|rythmic figure}})'', joué à la guitare sous forme d'accords de puissance ''({{lang|en|power chords}})'', c'est-à-dire des accords sans tierce. Le morceau est en tonalité de ''sol'' mineur naturel (donc avec un ''fa''♮) avec ajout de la note bleue (''{{lang|en|blue note}}'', quinte diminuée, ''ré''♭), et les accords composant le thème sont G<sup>5</sup>, B♭<sup>5</sup>, C<sup>5</sup> et D♭<sup>5</sup>, ce dernier accord étant l'accord sur la note bleue et pouvant être considéré comme une appoggiature (indiqué entre parenthèse ci-après). On a donc ''a priori'', sur les deux premières mesures, une progression {{Times New Roman|I-III-IV}} puis {{Times New Roman|I-III-(♭V)-IV}}. Durant la majeure partie du thème, la guitare basse tient la note ''sol'' en pédale. {{note|En jazz, la qualité « <sup>5</sup> » indique que l'on n'a que la quinte (et donc pas la tierce), contrairement à la notation de basse chiffrée.}} : {{lien web | url = https://www.dailymotion.com/video/x5ili04 | titre = Deep Purple — Smoke on the Water (Live at Montreux 2006) | auteur = Deep Purple | site = Dailymotion | date = 2016 | consulté le = 2020-12-31 }} Cependant, cette progression forme une mélodie, on peut donc plus la voir comme un contrepoint, la superposition de deux voies ayant un mouvement conjoint, joué par un seul instrument, la guitare, la voie 2 étant jouée une quarte juste en dessous de la voie 1 (la quarte juste descendante étant le renversement de la quinte juste ascendante) : * voie 1 (aigu) : | ''sol'' - ''si''♭ - ''do'' | ''sol'' - ''si''♭ - (''ré''♭) - ''do'' | ; * voie 2 (grave) : | ''ré'' - ''fa'' - ''sol'' | ''ré'' - ''fa'' - (''la''♭) - ''sol'' |. En se basant sur la basse (''sol'' en pédale), nous pouvons considérer que ces deux mesures sont accompagnées d'un accord de Gm<sup>7</sup> (''sol''-''si''♭-''ré''-''fa''), chaque accord de la mélodie comprenant à chaque fois au moins une note de cet accord à l'exception de l'appogiature. {| class="wikitable" |+ Mise en évidence des notes de l'accord Gm<sup>7</sup> |- ! scope="row" | Accords | G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> |- ! scope="row" | Voie 1 | '''''sol''''' || '''''si''♭''' || ''do'' |- ! scope="row" | Voie 2 | '''''ré''''' || '''''fa''''' || '''''sol''''' |- ! scope="row" | Basse | '''''sol''''' || '''''sol''''' || '''''sol''''' |} Sur les deux mesures suivantes, la basse varie et suit les accords de la guitare avec un retard sur le dernier accord : {| class="wikitable" |+ Voies sur les mesure 3-4 du thème |- ! scope="row" | Accords | G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> || B♭<sup>5</sup> || G<sup>5</sup> |- ! scope="row" | Voie 1 | ''sol'' || ''si''♭ || ''do'' || ''si''♭ || ''sol'' |- ! scope="row" | Voie 2 | ''ré'' || ''fa'' || ''sol'' || ''fa'' || ''ré'' |- ! scope="row" | Basse | ''sol'' || ''sol'' || ''do'' || ''si''♭ || ''si''♭-''sol'' |} Le couplet de cette chanson est aussi organisé sur une progression de quatre mesures, la guitare faisant des arpèges sur les accords G<sup>5</sup> (''sol''-''ré''-''sol'') et F<sup>5</sup> (''fa''-''do''-''fa'') : : | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-F<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | soit une progression {{Times New Roman|<nowiki>| I-I | I-I | I-VII | I-I |</nowiki>}}. Nous pouvons aussi harmoniser le riff du thème sur cette progression, avec un accord F (''fa''-''la''-''do'') ; nous pouvons aussi nous rappeler que l'accord sur le degré {{Times New Roman|VII}} est plus volontiers considéré comme un accord de septième de dominante {{Times New Roman|V<sup>7</sup>}}, soit ici un accord Dm<sup>7</sup> (''ré''-''fa''-''la''-''do''). On peut donc considérer la progression harmonique sur le thème : : | Gm-Gm | Gm-Gm | Gm-F ou Dm<sup>7</sup> | Gm-Gm |. Cette analyse permet de proposer une harmonisation enrichie du morceau, tout en se rappelant qu'une des forces du morceau initial est justement la simplicité de sa structure, qui fait ressortir la virtuosité des musiciens. Nous pouvons ainsi comparer la version album à la version concert avec orchestre ou à la version latino de Pat Boone. À l'inverse, le groupe Psychostrip, dans une version grunge, a remplacé les accords par une ligne mélodique : * le thème ne contient plus qu'une seule voie (la guitare ne joue pas des accords de puissance) ; * dans les mesures 9 et 10, la deuxième guitare joue en contrepoint de type mouvement inverse, qui est en fait la voie 2 jouée en miroir ; * l'arpège sur le couplet est remplacé par une ligne mélodique en ostinato sur une gamme blues. {| class="wikitable" |+ Contrepoint sur les mesures 9 et 10 |- ! scope="row" | Guitare 1 | ''sol'' ↗ ''si''♭ ↗ ''do'' |- ! scope="row" | Guitare 2 | ''sol'' ↘ ''fa'' ↘ ''ré'' |} * {{lien web | url = https://www.dailymotion.com/video/x5ik234 | titre = Deep Purple — Smoke on the Water (In Concert with the London Symphony Orchestra, 1999) | auteur = Deep Purple | site = Dailymotion | date = 2016 | consulté le = 2020-12-31 }} * {{lien web | url = https://www.youtube.com/watch?v=MtUuNzVROIg | titre = Pat Boone — Smoke on the Water (In a Metal Mood, No More Mr. Nice Guy, 1997) | auteur = Orrore a 33 Giri | site = YouTube | date = 2019-06-24 | consulté le = 2020-12-31 }} * {{lien web | url = https://www.youtube.com/watch?v=n7zLlZ8B0Bk | titre = Smoke on the Water (Heroes, 1993) | auteur = Psychostrip | site = YouTube | date = 2018-06-20 | consulté le = 2020-12-31 }} == Accords et improvisation == Nous avons vu précédemment (chapitre ''[[../Gammes et intervalles#Modes et improvisation|Gammes et intervalles &gt; Modes et improvisation]]'') que le choix d'un mode adapté permet d'improviser sur un accord. L'harmonisation des gammes permet, en inversant le processus, d'étendre notre palette : il suffit de repérer l'accord sur une harmonisaiton de gamme, et d'utiliser cette gamme-là, dans le mode correspondant du degré de l'accord (voir ci-dessus ''[[#Harmonisation par des accords de septième|Harmonisation par des accords de septième]]''). Par exemple, nous avons vu que l'accord sur le septième degré d'une gamme majeure était un accord demi-diminué ; nous savons donc que sur un accord demi-diminué, nous pouvons improviser sur le mode correspondant au septième degré, soit le mode de ''si'' (locrien). Un accord de septième de dominante étant commun aux deux tonalités homonymes (par exemple ''fa'' majeur et ''fa'' mineur pour un ''do''<sup>7</sup><sub>+</sub> / C<sup>7</sup>), nous pouvons utiliser le mode de ''sol'' de la gamme majeure (mixolydien) ou de la gamme mineure mineure (mode phrygien dominant, ou phrygien espagnol) pour improviser. Mais l'accord de septième de dominante est aussi l'accord au début d'une grille blues ; on peut donc improviser avec une gamme blues, même si la tierce est majeure dans l'accord et mineure dans la gamme. [[Fichier:Mode improvisation accords do complet.svg]] == Autres accords courants == [[fichier:Cluster cdefg.png|vignette|Agrégat ''do - ré - mi - fa - sol''.]] Nous avons vu précédemment l'harmonisation des tonalités majeures et mineures harmoniques par des triades et des accords de septième ; certains accords étant rarement utilisés (l'accord sur le degré {{Times New Roman|III}} et, pour les tonalités mineures harmoniques, l'accord sur la tonique), certains accords étant utilisés comme des accords sur un autre degré (les accords sur la sensible étant considérés comme des accords de dominante sans fondamentale). Dans l'absolu, on peut utiliser n'importe quelle combinaison de notes, jusqu'aux agrégats, ou ''{{lang|en|clusters}}'' (mot anglais signifiant « amas », « grappe ») : un ensemble de notes contigües, séparées par des intervalles de seconde. Dans la pratique, on reste souvent sur des accords composés de superpositions de tierces, sauf dans le cas de transitions (voir la section ''[[#Notes étrangères|Notes étrangère]]''). === En musique classique === On utilise parfois des accords dont les notes ne sont pas dans la tonalité (hors modulation). Il peut s'agir d'accords de passage, de notes étrangères, par exemple utilisant un chromatisme (mouvement conjoint par demi-tons). Outre les accords de passage, les autres accords que l'on rencontre couramment en musique classique sont les accords de neuvième, et les accords de onzième et treizième sur tonique. Ces accords sont simplement obtenus en continuant à empiler les tierces. Il n'y a pas d'accord d'ordre supérieur car la quinzième est deux octaves au-dessus de la fondamentale. Comme pour les accords de septième, on distingue les accords de neuvième de dominante et les accords de neuvième d'espèce. Dans le cas de la neuvième de dominante, il y a une différence entre les tonalités majeures et mineures : l'intervalle de neuvième est respectivement majeur et mineur. Les chiffrages des renversements peuvent donc différer. Comme pour les accords de septième de dominante, on considère que les accords de septième sur le degré {{Times New Roman|VI}} sont en fait des accords de neuvième de dominante sans fondamentale. Les accords de neuvième d'espèce sont en général préparés et résolus. Préparés : la neuvième étant une note dissonante (c'est à une octave près la seconde de la fondamentale), l'accord qui précède doit contenir cette note, mais dans un accord consonant ; la neuvième est donc commune avec l'accord précédent. Résolus : la dissonance est résolue en abaissant la neuvième par un mouvement conjoint. Par exemple, en tonalité de ''do'' majeur, si l'on veut utiliser un accord de neuvième d'espèce sur la tonique ''(do - mi - sol - si - ré)'', on peut utiliser avant un accord de dominante ''(sol - si - ré)'' en préparation puis un accord parfait sur le degré {{Times New Roman|IV}} ''(fa - la - do)'' en résolution ; nous avons donc sur la voie la plus aigüe la succession ''ré'' (consonant) - ''ré'' (dissonant) - ''do'' (consonant). On rencontre également parfois des accords de onzième et de treizième. On omet en général la tierce, car elle est dissonante avec la onzième. L'accord le plus fréquemment rencontré est l'accord sur la tonique : on considère alors que c'est un accord sur la dominante que l'on a enrichi « par le bas », en ajoutant une quinte inférieure. par exemple, dans la tonalité de ''do'' majeur, l'accord ''do - sol - si - ré - fa'' est considéré comme un accord de septième de dominante sur tonique, le degré étant noté « {{Times New Roman|V}}/{{Times New Roman|I}} ». De même pour l'accord ''do - sol - si - ré - fa - la'' qui est considéré comme un accord de neuvième de dominante sur tonique. === En jazz === En jazz, on utilise fréquemment l'accord de sixte à la place de l'accord de septième majeure sur la tonique. Par exemple, en ''do'' majeur, on utilise l'accord C<sup>6</sup> ''(do - mi - sol - la)'' à la place de C<sup>Δ</sup> ''(do - mi - sol - si)''. On peut noter que C<sup>6</sup> est un renversement de Am<sup>7</sup> et pourrait donc se noter Am<sup>7</sup>/C ; cependant, le fait de le noter C<sup>6</sup> indique que l'on a bien un accord sur la tonique qui s'inscrit dans la tonalité de ''do'' majeur (et non, par exemple, de ''la'' mineur naturelle) — par rapport à l'harmonie fonctionnelle, on remarquera que Am<sup>7</sup> a une fonction tonique, l'utilisation d'un renversement de Am<sup>7</sup> à la place d'un accord de C<sup>Δ</sup> est donc logique. Les accords de neuvième, onzième et treizième sont utilisés comme accords de septième enrichis. Le chiffrage suit les règles habituelles : on ajoute un « 9 », un « 11 » ou un « 13 » au chiffrage de l'accord de septième. On utilise également des accords dits « suspendus » : ce sont des accords de transition qui sont obtenus en prenant une triade majeure ou mineure et en remplaçant la tierce par la quarte juste (cas le plus fréquent) ou la seconde majeure. Plus particulièrement, lorsque l'on parle simplement « d'accord suspendu » sans plus de précision, cela désigne l'accord de neuvième avec une quarte suspendue, noté « 9sus4 » ou simplement « sus ». == L'harmonie tonale == L'harmonie tonale est un ensemble de règle assez strictes qui s'appliquent dans la musique savante européenne, de la période baroque à la période classique classique ({{pc|xiv}}<sup>e</sup>-{{pc|xviii}}<sup>e</sup> siècle). Certaines règles sont encore largement appliquées dans divers styles musicaux actuels, y compris populaire (rock, rap…), d'autres sont au contraire ignorées (par exemple, un enchaînement de plusieurs accords de même qualité forme un mouvement parallèle, ce qui est proscrit en harmonie tonale). De nos jours, on peut voir ces règles comme des règles « de bon goût », et leur application stricte comme une manière de composer « à la manière de ». Précédemment, nous avons vu la progression des accords. Ci-après, nous abordons aussi la manière dont les notes de l'accord sont réparties entre plusieurs voix, et comment on construit chaque voix. === Concepts fondamentaux === ; Consonance : Les intervalles sont considérés comme « plus ou moins consonants » : :* consonance parfaite : unisson, quinte et octave ; :* consonance mixte (parfaite dans certains contextes, imparfaite dans d'autres) : quarte ; :* consonance imparfaite : tierce et sixte ; :* dissonance : seconde et septième. ; Degrés : Certains degrés sont considérés comme « forts », « meilleurs », ce sont les « notes tonales » : {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (sous-dominante) et {{Times New Roman|V}} (dominante). [[Fichier:Mouvements harmoniques.svg|vignette|upright=0.75|Mouvements harmoniques.]] ; Mouvements : Le mouvement décrit la manière dont les voix évoluent les unes par rapport aux autres : :# Mouvement parallèle : les voix sont séparées par un intervalle constant. :# Mouvement oblique : une voix reste constante, c'est le bourdon ; l'autre monte ou descend. :# Mouvement contraire : une voix descend, l'autre monte. :# Échange de voix : les voix échangent de note ; les mélodies se croisent mais on a toujours le même intervalle harmonique. {{clear}} === Premières règles === ; Règle du plus court chemin : Quand on passe d'un accord à l'autre, la répartition des notes se fait de sorte que chaque voix fait le plus petit mouvement possible. Notamment : si les deux accords ont des notes en commun, alors les voix concernées gardent la même note. : Les deux voix les plus importantes sont la voix aigüe — soprano — et la voix la plus grave — basse. Ces deux voix sont relativement libres : la voix de soprano a la mélodie, la voix de basse fonde l'harmonie. La règle du plus court chemin s'applique surtout aux voix intermédiaires ; si l'on a des mouvements conjoints, ou du moins de petits intervalles — c'est le sens de la règle du plus court chemin —, alors les voix sont plus faciles à interpréter. Cette règle évite également que les voix n'empiètent l'une sur l'autre (voir la règle « éviter le croisement des voix »). ; Éviter les consonances parfaites consécutives :* Lorsque deux voix sont à l'unisson ou à l'octave, elles ne doivent pas garder le même intervalle, l'effet serait trop plat. :* Lorsque deux voix sont à la quarte ou à la quinte, elles ne doivent pas garder le même intervalle, car l'effet est trop dur. : Pour éviter cela, lorsque l'on part d'un intervalle juste, on a intérêt à pratiquer un mouvement contraire aux voix qui ne gardent pas la même note, ou au moins un mouvement direct : les voix vont dans le même sens, mais l'intervalle change. : Notez que même avec le mouvement contraire, on peut avoir des consonances parfaites consécutives, par exemple si une voix fait ''do'' aigu ↗ ''sol'' aigu et l'autre ''sol'' médium ↘ ''do'' grave. : L'interdiction des consonances parfaites consécutives n'a pas été toujours appliquée, le mouvement parallèle strict a d'ailleurs été le premier procédé utilisé dans la musique religieuse au {{pc|x}}<sup>e</sup> siècle. On peut par exemple utiliser des quintes parallèles pour donner un style médiéval au morceau. On peut également utiliser des octaves parallèles sur plusieurs notes afin de créer un effet de renforcement de la mélodie. : Par ailleurs, les consonances parfaites consécutives sont acceptées lorsqu'il s'agit d'une cadence (transition entre deux parties ou bien conclusion du morceau). ; Éviter le croisement des voix : Les voix sont organisées de la plus grave à la plus aigüe. Deux voix n'étant pas à l'unisson, celle qui est plus aigüe ne doit pas devenir la plus grave et ''vice versa''. ; Soigner la partie soprano : Comme c'est celle qu'on entend le mieux, c'est en général celle qui porte la mélodie principale. On lui applique des règles spécifiques : :# Si elle chante la sensible dans un accord de dominante ({{Times New Roman|V}}), alors elle doit monter à la tonique, c'est-à-dire que la note suivante sera la tonique située un demi-ton au dessus. :# Si l'on arrive à une quinte ou une octave entre les parties basse et soprano par un mouvement direct, alors sur la partie soprano, le mouvement doit être conjoint. On doit donc arriver à cette situation par des notes voisines au soprano. ; Préférer certains accords : Les deux degrés les plus importants sont la tonique ({{Times New Roman|I}}) et la dominante ({{Times New Roman|V}}), les accords correspondants ont donc une importance particulière. : À l'inverse, l'accord de sensible ({{Times New Roman|VII}}) n'est pas considéré comme ayant une fonction harmonique forte. On le considère comme un accord de dominante affaibli. En tonalité mineure, on évite également l'accord de médiante ({{Times New Roman|III}}). : Donc on utilise en priorité les accords de : :# {{Times New Roman|I}} et {{Times New Roman|V}}. :# Puis {{Times New Roman|II}}, {{Times New Roman|IV}}, {{Times New Roman|VI}} ; et {{Times New Roman|III}} en mode majeur. :# On évite {{Times New Roman|VII}} ; et {{Times New Roman|III}} en mode mineur. ; Préférer certains enchaînements : Les enchaînements d'accord peuvent être classés par ordre de préférence. Par ordre de préférence décroissante (du « meilleur » au « moins bon ») : :# Meilleurs enchaînements : quarte ascendante ou descendante. Notons que la quarte est le renversement de la quinte, on a donc des enchaînements stables et naturels, mais avec un intervalle plus court qu'un enchaînement de quintes. :# Bons enchaînements : tierce ascendante ou descendante. Les accords consécutifs ont deux notes en commun. :# Enchaînements médiocres : seconde ascendante ou descendante. Les accords sont voisins, mais ils n'ont aucune note en commun. On les utilise de préférence en mouvement ascendant, et on utilise surtout les enchaînements {{Times New Roman|IV}}-{{Times New Roman|V}}, {{Times New Roman|V}}-{{Times New Roman|VI}} et éventuellement {{Times New Roman|I}}-{{Times New Roman|II}}. :# Les autres enchaînements sont à éviter. : On peut atténuer l'effet d'un enchaînement médiocre en plaçant le second accord sur un temps faible ou bien en passant par un accord intermédiaire. [[Fichier:Progression Vplus4 I6.svg|thumb|Résolution d'un accord de triton (quarte sensible) vers l'accord de sixte de la tonique.]] ; La septième descend par mouvement conjoint : Dans un accord de septième de dominante, la septième — qui est donc le degré {{Times New Roman|IV}} — descend par mouvement conjoint — elle est donc suivie du degré {{Times New Roman|III}}. : Corolaire : un accord {{Times New Roman|V}}<sup>+4</sup> se résout par un accord {{Times New Roman|I}}<sup>6</sup> : on a bien un enchaînement {{Times New Roman|V}} → {{Times New Roman|I}}, et la 7{{e}} (degré {{Times New Roman|IV}}), qui est la basse de l'accord {{Times New Roman|V}}<sup>+4</sup>, descend d'un degré pour donner la basse de l'accord {{Times New Roman|I}}<sup>6</sup> (degré {{Times New Roman|III}}). {{clear}} [[Fichier:Progression I64 V7plus I5.svg|thumb|Accord de sixte et de quarte cadentiel.]] ; Un accord de sixte et quarte est un accord de passage : Le second renversement d'un accord parfait est soit une appoggiature, soit un accord de passage, soit un accord de broderie. : S'il s'agit de l'accord de tonique {{Times New Roman|I}}<sup>6</sup><sub>4</sub>, c'est « accord de sixte et quarte de cadence », l'appoggiature de l'accord de dominante de la cadence parfaite. {{clear}} Mais il faut appliquer ces règles avec discernement. Par exemple, la voix la plus aigüe est celle qui s'entend le mieux, c'est donc elle qui porte la mélodie principale. Il est important qu'elle reste la plus aigüe. La voix la plus grave porte l'harmonie, elle pose les accords, il est donc également important qu'elle reste la plus grave. Ceci a deux conséquences : # Ces deux voix extrêmes peuvent avoir des intervalles mélodiques importants et donc déroger à la règle du plus court chemin : la voix aigüe parce que la mélodie prime, la voix de basse parce que la progression d'accords prime. # Les croisements des voix intermédiaires sont moins critiques. Par ailleurs, si l'on applique strictement toutes les règles « meilleurs accords, meilleurs enchaînements », on produit un effet conventionnel, stéréotypé. Il est donc important d'utiliser les solutions « moins bonnes », « médiocres » pour apporter de la variété. Ajoutons que les renversements d'accords permettent d'avoir plus de souplesse : on reste sur le même accord, mais on enrichit la mélodie sur chaque voix. Le ''Bolero'' de Maurice Ravel (1928) brise un certain nombre de ces règles. Par exemple, de la mesure 39 à la mesure 59, la harpe joue des secondes. De la mesure 149 à la mesure 165, les piccolo jouent à la sixte, dans des mouvement strictement parallèle, ce qui donne d'ailleurs une sonorité étrange. À partir de la mesure 239, de nombreux instruments jouent en mouvement parallèles (piccolos, flûtes, hautbois, cor, clarinettes et violons). === Application === [[Fichier:Harmonisation possible de frere jacques exercice.svg|vignette|Exercice : harmoniser ''Frère Jacques''.]] Harmoniser ''Frère Jacques''. Nous considérons un morceau à quatre voix : basse, ténor, alto et soprano. La soprano chante la mélodie de ''Frère Jacques''. L'exercice consiste à proposer l'écriture des trois autres voix en respectant les règles énoncées ci-dessus. Pour simplifier, nous ajoutons les contraintes suivantes : * toutes les voix chantent des blanches ; * nous nous limitons aux accords de quinte (accords de trois sons composés d'une tierce et d'une quinte) sans avoir recours à leurs renversements (accords de sixte, accords de sixte et de quarte). Les notes à gauche de la portée indiquent la tessiture (ou ambitus), l'amplitude que peut chanter la voix. {{clear}} {{boîte déroulante/début|titre=Solution possible}} [[Fichier:Harmonisation possible de frere jacques solution.svg|vignette|Harmonisation possible de ''Frère Jacques'' (solution de l'exercice).]] Il n'y a pas qu'une solution possible. Le premier accord doit contenir un ''do''. Nous sommes manifestement en tonalité de ''do'' majeur, nous proposons de commencer par l'accord parfait de ''do'' majeur, I<sup>5</sup>. Le deuxième accord doit comporter un ''ré''. Si nous utilisons l'accord de quinte de ''ré'', nous allons créer une quinte parallèle. Nous pourrions utiliser un renversement, mais nous nous imposons de chercher un autre accord. Il peut s'agir de l'accord ''si''<sup>5</sup> ''(si-ré-fa)'' ou de l'accord de ''sol''<sup>5</sup> ''(sol-si-ré)''. La dernière solution permet d'utiliser l'accord de dominante qui est un accord important de la tonalité. La règle du plus court chemin imposerait le ''sol'' grave pour la partie de basse, mais cela est proche de la limite du chanteur, nous préférons passer au ''sol'' aigu, plus facile à chanter. Nous vérifions qu'il n'y a pas de quinte parallèle : l'intervalle ascendant ''do-sol'' (basse-alto) devient ''sol-si'' (3<sup>ce</sup>), l'intervalle descendant ''do-sol'' (soprano-alto) devient ''ré-si'' (3<sup>ce</sup>). De la même manière, pour le troisième accord, nous ne pouvons pas passer à un accord de ''la''<sup>5</sup> pour éviter une quinte parallèle. Nous avons le choix entre ''do''<sup>5</sup> ''(do-mi-sol)'' et ''mi''<sup>5</sup> ''(mi-sol-si)''. Nous préférons revenir à l'accord de fondamental, solution très stable (l'enchaînement {{Times New Roman|V}}-{{Times New Roman|I}} formant une cadence parfaite). Pour le quatrième accord, nous pourrions rester sur l'accord parfait de ''do'' mais cela planterait en quelque sorte la fin du morceau puisque l'on resterait sur la cadence parfaite ; or, nous connaissons le morceau et savons qu'il n'est pas fini. Nous choisissons l'accord de ''la''<sup>5</sup> qui est une sixte ascendante ({{Times New Roman|I}}-{{Times New Roman|VI}}). Nos aurions pu répartir les voix différemment. Par exemple : * alto : ''sol''-''si''-''sol''-''do'' ; * ténor : ''mi''-''ré''-''mi''-''mi''. {{boîte déroulante/fin}} [[Fichier:Harmonisation possible de frere jacques.midi|vignette|Fichier son correspondant.]] {{clear}} == Annexe == === Accords en musique classique === Un accord est un ensemble de notes jouées simultanément. Il peut s'agir : * de notes jouées par plusieurs instruments ; * de notes jouées par un même instrument : piano, clavecin, orgue, guitare, harpe (la plupart des instruments à clavier et des instruments à corde). Pour deux notes jouées simultanément, on parle d'intervalle « harmonique » (par opposition à l'intervalle « mélodique » qui concerne les notes jouées successivement). Les notes répétées à différentes octaves ne changent pas la nature de l'accord. La musique classique considère en général des empilements de tierces ; un accord de trois notes sera constitué de deux tierces successives, un accord de quatre notes de trois tierces… Lorsque tous les intervalles sont des intervalles impairs — tierces, quintes, septièmes, neuvièmes, onzièmes, treizièmes… — alors l'accord est dit « à l'état fondamental » (ou encore « primitif » ou « direct »). La note de la plus grave est appelée « fondamentale » de l'accord. Lorsque l'accord comporte un ou des intervalles pairs, l'accord est dit « renversé » ; la note la plus grave est appelée « basse ». De manière plus générale, l'accord est dit à l'état fondamental lorsque la basse est aussi la fondamentale. On a donc un état idéal de l'accord (état canonique) — un empilement strict de tierces — et l'état réel de l'accord — l'empilement des notes réellement jouées, avec d'éventuels redoublements, omissions et inversions ; et seule la basse indique si l'accord est à l'état fondamental ou renversé. Le chiffrage dit de « basse continue » ''({{lang|it|basso continuo}})'' désigne la représentation d'un accord sous la forme d'un ou plusieurs chiffres arabes et éventuellement d'un chiffre romain. ==== Accords de trois notes ==== En musique classique, les seuls accords considérés comme parfaitement consonants, c'est-à-dire sonnant agréablement à l'oreille, sont appelés « accords parfaits ». Si l'on prend une tonalité et un mode donné, alors l'accord construit par superposition es degrés I, III et V de cette gamme porte le nom de la gamme qui l'a généré. [[fichier:Accord do majeur chiffre.svg|vignette|upright=0.5|Accord parfait de ''do'' majeur chiffré.]] Par exemple : * « l'accord parfait de ''do'' majeur » est composé des notes ''do'', ''mi'' et ''sol'' ; * « l'accord parfait de ''la'' mineur » est composé des notes ''la'', ''do'' et ''mi''. Un accord parfait majeur est donc composé, en partant de la fondamentale, d'une tierce majeure et d'une quinte juste. Un accord parfait mineur est composé d'une tierce mineure et d'une quinte juste. L'accord parfait à l'état fondamental est appelé « accord de quinte » et est simplement chiffré « 5 » pour indiquer la quinte. On peut également commencer un accord sur sa deuxième ou sa troisième note, en faisant monter celle(s) qui précède(nt) à l'octave suivante. On parle alors de « renversement d'accord » ou d'accord « renversé ». [[Fichier:Accord do majeur renversements chiffre.svg|vignette|upright=0.75|Accord parfait de ''do'' majeur et ses renversements, chiffrés.]] Par exemple, * le premier renversement de l'accord parfait de ''do'' majeur est :<br /> ''mi'', ''sol'', ''do'' ; * le second renversement de l'accord parfait de do majeur est :<br /> ''sol'', ''do'', ''mi''. Les notes conservent leur nom de « fondamentale », « tierce » et « quinte » malgré le changement d'ordre. La note la plus grave est appelée « basse ». Dans le cas du premier renversement, le deuxième note est la tierce de la basse (la note la plus grave) et la troisième note est la sixte ; le chiffrage en chiffres arabes est donc « 6 » (puisque l'on omet la tierce) et l'accord est appelé « accord de sixte ». Pour le deuxième renversement, les intervalles sont la quarte et la sixte, le chiffrage est donc « 6-4 » et l'accord est appelé « accord de sixte et de quarte ». Dans tous les cas, on chiffre le degré on considérant la fondamentale, par exemple {{Times New Roman|I}} si l'accord est construit sur la tonique de la gamme. Les autres accords de trois notes que l'on rencontre sont : * l'accord de quinte diminuée, constitué d'une tierce mineure et d'une quinte diminuée ; lorsqu'il est construit sur le septième degré d'une gamme, on considère que c'est un accord de septième de dominante sans fondamentale (voir plus bas), le degré est donc indiqué « “{{Times New Roman|V}}” » (cinq entre guillemets) et non « {{Times New Roman|VII}} » ; * l'accord de quinte augmenté : il est composé d'une tierce majeure et qu'une quinte augmentée. Dans le tableau ci-dessous, * « m » désigne un intervalle mineur ; * « M » un intervalle majeur ou le mode majeur ; * « J » un intervalle juste ; * « d » un intervalle diminué ; * « A » un intervalle augmenté ; * « mh » le mode mineur harmonique ; * « ma » le mode mineur ascendant ; * « md » le mode mineur descendant. {| class="wikitable" |+ Accords de trois notes ! scope="col" rowspan="2" | Nom ! scope="col" rowspan="2" | 3<sup>ce</sup> ! scope="col" rowspan="2" | 5<sup>te</sup> ! scope="col" rowspan="2" | État fondamental ! scope="col" rowspan="2" | 1<sup>er</sup> renversement ! scope="col" rowspan="2" | 2<sup>nd</sup> renversement ! scope="col" colspan="4"| Construit sur les degrés |- ! scope="col" | M ! scope="col" | mh ! scope="col" | ma ! scope="col" | md |- | Accord parfait<br /> majeur || M || J | accord de quinte || accord de sixte || accord de<br />sixte et de quarte | {{Times New Roman|I, IV, V}} || {{Times New Roman|V, VI}} || {{Times New Roman|IV, V}} || {{Times New Roman|III, VI, VII}} |- | Accord parfait<br /> mineur || m || J | accord de quinte || accord de sixte || accord de<br />sixte et de quarte | {{Times New Roman|II, III, VI}} || {{Times New Roman|I, IV}} || {{Times New Roman|I, II}} || {{Times New Roman|I, IV, V}} |- | Accord de<br />quinte diminuée || m || d | accord de<br />quinte diminuée || accord de<br />sixte sensible<br />sans fondamentale || accord de triton<br />sans fondamentale | {{Times New Roman|VII (“V”)}} || {{Times New Roman|II, VII (“V”)}} || {{Times New Roman|VI, VII (“V”)}} || {{Times New Roman|II}} |- | Accord de<br />quinte augmentée || M || A | accord de<br />quinte augmentée || accord de sixte<br />et de tierce sensible || accord de sixte et de quarte<br />sur sensible | &nbsp; || {{Times New Roman|III}} || {{Times New Roman|III}} || &nbsp; |} ==== Accords de quatre notes ==== Les accords de quatre notes sont des accord composés de trois tierces superposées. La dernière note étant le septième degré de la gamme, on parle aussi d'accords de septième. Ces accords sont dissonants : ils contiennent un intervalle de septième (soit une octave montante suivie d'une seconde descendante). Ils laissent donc une impression de « tension ». Il existe sept différents types d'accords, ou « espèces ». Citons l'accord de septième de dominante, l'accord de septième mineure et l'accord de septième majeure. ===== L'accord de septième de dominante ===== [[Fichier:Accord 7e dominante do majeur renversements chiffre.svg|vignette|Accord de septième de dominante de ''do'' majeur et ses renversements, chiffrés.]] L'accord de septième de dominante est l'empilement de trois tierces à partir de la dominante de la gamme, c'est-à-dire du {{Times New Roman|V}}<sup>e</sup> degré. Par exemple, l'accord de septième de dominante de ''do'' majeur est l'accord ''sol''-''si''-''ré''-''fa'', et l'accord de septième de dominante de ''la'' mineur est ''mi''-''sol''♯-''si''-''ré''. L'accord de septième de dominante dont la fondamentale est ''do'' (''do''-''mi''-''sol''-''si''♭) appartient à la gamme de ''fa'' majeur. Que le mode soit majeur ou mineur, il est composé d'une tierce majeure, d'une quinte juste et d'une septième mineure (c'est un accord parfait majeur auquel on ajoute une septième mineure). C'est de loin l'accord de septième le plus utilisé ; il apparaît au {{pc|xvii}}<sup>e</sup> en musique classique. Dans son état fondamental, son chiffrage est {{Times New Roman|V 7/+}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}). Le signe plus indique la sensible. Son premier renversement est appelé « accord de quinte diminuée et sixte » et est noté {{Times New Roman|V 6/<s>5</s>}} (ou {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}). Son deuxième renversement est appelé « accord de sixte sensible », puisque la sixte de l'accord est la sensible de la gamme, et est noté {{Times New Roman|V +6}} (ou {{Times New Roman|V<sup>+6</sup>}}). Son troisième renversement est appelé « accord de quarte sensible » et est noté {{Times New Roman|V +4}} (ou {{Times New Roman|V<sup>+4</sup>}}). [[Fichier:Accord 7e dominante sans fondamentale do majeur renversements chiffre.svg|vignette|Accord de septième de dominante sans fondamentale de ''do'' majeur et ses renversements, chiffrés.]] On utilise aussi l'accord de septième de dominante sans fondamentale ; c'est alors un accord de trois notes. Dans son état fondamental, c'est un « accord de quinte diminuée » placé sur le {{Times New Roman|VII}}<sup>e</sup> degré (mais c'est bien un accord construit sur le {{Times New Roman|V}}<sup>e</sup> degré), noté {{Times New Roman|“V” <s>5</s>}} (ou {{Times New Roman|“V”<sup><s>5</s></sup>}}). Notez les guillemets qui indiquent que la fondamentale V est absente. Dans son premier renversement, c'est un « accord de sixte sensible sans fondamentale » noté {{Times New Roman|“V” +6/3}} (ou {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}}). Dans son second renversement, c'est un « accord de triton sans fondamentale » (puisque le premier intervalle est une quarte augmentée qui comporte trois tons) noté {{Times New Roman|“V” 6/+4}} (ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}). Notons qu'un accord de septième de dominante n'a pas toujours la dominante pour fondamentale : tout accord composé d'une tierce majeure, d'une quinte juste et d'une septième mineure est un accord de septième de dominante et est chiffré {{Times New Roman|<sup>7</sup><sub>+</sub>}}, quel que soit le degré sur lequel il est bâti (certaines notes peuvent avoir une altération accidentelle). ===== Les accords de septième d'espèce ===== Les autres accords de septièmes sont dits « d'espèce ». L'accord de septième mineure est l'accord de septième formé sur la fondamentale d'une gamme mineure ''naturelle''. Par exemple, l'accord de septième mineure de ''la'' est ''la''-''do''-''mi''-''sol''. Il est composé d'une tierce mineure, d'une quinte juste et d'une septième mineure (c'est un accord parfait mineur auquel on ajoute une septième mineure). L'accord de septième majeure est l'accord de septième formé sur la fondamentale d'une gamme majeure. Par exemple, L'accord de septième majeure de ''do'' est ''do''-''mi''-''sol''-''si''. Il est composé d'une tierce majeure, d'une quinte juste et d'une septième majeure (c'est un accord parfait majeur auquel on ajoute une septième majeure). ==== Utilisation du chiffrage ==== Le chiffrage est utilisé de deux manières. La première manière, c'est la notation de la basse continue. La basse continue est une technique d'improvisation utilisée dans le baroque pour l'accompagnement d'instruments solistes. Sur la partition, on indique en général la note de basse de l'accord et le chiffrage en chiffres arabes. La seconde manière, c'est pour l'analyse d'une partition. Le fait de chiffrer les accords permet de mieux en comprendre la structure. De manière générale, on peut retenir que : * le chiffrage « 5 » indique un accord parfait, superposition d'une tierce (majeure ou mineure) et d'une quinte juste ; * le chiffrage « 6 » indique le premier renversement d'un accord parfait ; * le chiffrage « 6/4 » indique le second renversement d'un accord parfait ; * chiffrage « 7/+ » indique un accord de septième de dominante ; * le signe « + » indique en général que la note de l'intervalle est la sensible ; * un intervalle barré désigne un intervalle diminué. [[fichier:Accords gamme do majeur la mineur.svg|class=transparent| center | Principaux accords construits sur les gammes de ''do'' majeur et de ''la'' mineur harmonique.]] === Notation « jazz » === En jazz et de manière générale en musique rock et populaire, la base d'un accord est la triade composée d'une tierce (majeure ou mineure) et d'une quinte juste. Pour désigner un accord, on utilise la note fondamentale, éventuellement désigné par une lettre dans le système anglo-saxon (A pour ''la'' etc.), suivi d'une qualité (comme « m », « + »…). Les renversements ne sont pas notés de manière particulière, ils sont notés comme les formes fondamentales. Dans les deux tableaux suivants, la fondamentale est notée X (remplace le C pour un accord de ''do'', le D pour un accord de ''ré''…). La construction des accords est décrite par la suite. [[Fichier:Arbre accords triades 5d5J5A.svg|vignette|upright=1.5|Formation des triades présentée sous forme d'arbre.]] {| class="wikitable" |+ Notation des principales triades |- | ! scope="col" | Tierce<br />mineure (3m) ! scope="col" | Tierce<br />majeure (3M) |- ! scope="row" | Quinte diminuée (5d) | X<sup>o</sup>, Xm<sup>♭5</sup>, X–<sup>♭5</sup> || |- ! scope="row" | Quinte juste (5J) | Xm, X– || X |- ! scope="row" | Quinte augmentée (5A) | || X+, X<sup>♯5</sup> |} [[Fichier:Triades do.svg|class=transparent|center|Triades de do.]] {| class="wikitable" |+ Notation des principaux accords de septième |- | colspan="2" | ! scope="col" | Tierce<br />mineure (3m) ! scope="col" | Tierce<br />majeure (3M) |- ! scope="row" rowspan="2" | Quinte<br />diminuée (5d) ! scope="row" | Septième diminuée (7d) | X<sup>o7</sup> || |- ! scope="row" | Septième mineure (7m) | Xm<sup>7(♭5)</sup>, X–<sup>7(♭5)</sup>, X<sup>Ø</sup> || |- ! scope="row" rowspan="3" | Quinte<br />juste (5J) ! scope="row" | Sixte majeure (6M) | Xm<sup>6</sup> || X<sup>6</sup> |- ! scope="row" | Septième mineure (7m) | Xm<sup>7</sup>, X–<sup>7</sup> || X<sup>7</sup> |- ! scope="row" | Septième majeure (7M) | Xm<sup>maj7</sup>, X–<sup>maj7</sup>, Xm<sup>Δ</sup>, X–<sup>Δ</sup> || X<sup>maj7</sup>, X<sup>Δ</sup> |- ! scope="row" rowspan="2" | Quinte<br />augmentée (5A) ! scope="row" | Septième mineure (7m) | || X+<sup>7</sup> |- ! scope="row" | Septième majeure (7M) | || X+<sup>maj7</sup> |} [[Fichier:Arbre accords septieme.svg|class=transparent|center|Formation des accords de septième présentée sous forme d'arbre.]] &nbsp; &nbsp; [[Fichier:Accords do septieme.svg|class=transparent|center|Accord de do septième.]] On notera que l'intervalle de sixte majeure est l'enharmonique de celui de septième diminuée (6M = 7d). [[File:Principaux accords do.svg|class=transparent|center|Principaux accords de do.]] ==== Triades ==== ; Accords fondés sur une tierce majeure * accord parfait majeur : pas de notation *: p. ex. « ''do'' » ou « C » pour l'accord parfait de ''do'' majeur (''do'' - ''mi'' - ''sol'') ; Accords fondés sur une tierce mineure * accord parfait mineur : « m », « min » ou « – » *: « ''do'' m », « ''do'' – », « Cm », « C– »… pour l'accord parfait de ''do'' mineur (''do'' - ''mi''♭ - ''sol'') ==== Triades modifiées ==== ; Accords fondés sur une tierce majeure * accord augmenté (la quinte est augmentée) : aug, +, ♯5 *: « ''do'' aug », « ''do'' + », « ''do''<sup>♯5</sup> » « Caug », « C+ » ou « C<sup>♯5</sup> » pour l'accord de ''do'' augmenté (''do'' - ''mi'' - ''sol''♯) : L'accord augmenté est un empilement de tierces majeures. Ainsi, un accord augmenté a deux notes communes avec deux autres accords augmentés : C+ (''do'' - ''mi'' - ''sol''♯) a deux notes communes avec A♭+ (''la''♭ - ''do'' - ''mi'') et avec E+ (''mi'' - ''sol''♯ - ''si''♯) ; et on remarque que ces trois accords sont en fait enharmoniques (avec les enharmonies ''la''♭ = ''sol''♯ et ''si''♯ = ''do''). En effet, l'octave comporte six tons (sous la forme de cinq tons et deux demi-tons), et une tierce majeure comporte deux tons, on arrive donc à l'octave en ajoutant une tierce majeure à la dernière note de l'accord. ; Accords fondés sur une tierce mineure * accord diminué (la quinte est diminuée) : dim, o, ♭5 *: « ''do'' dim », « ''do''<sup>o</sup> », « ''do''<sup>♭5</sup> », « Cdim », « C<sup>o</sup> » ou « C<sup>♭5</sup> » pour l'accord de ''do'' diminuné (''do'' - ''mi''♭ - ''sol''♭) : On remarque que la quinte diminuée est l'enharmonique de la quarte augmentée et est l'intervalle appelé « triton » (car composé de trois tons). ; Accords fondés sur une tierce majeure ou mineure * accord suspendu de seconde : la tierce est remplacée par une seconde majeure : sus2 *: « ''do''<sup>sus2</sup> » ou « C<sup>sus2</sup> » pour l'accord de ''do'' majeur suspendu de seconde (''do''-''ré''-''sol'') * accord suspendu de quarte : la tierce est remplacée par une quarte juste : sus4 *: « ''do''<sup>sus4</sup> » ou « C<sup>sus4</sup> » pour l'accord de ''do'' majeur suspendu de quarte (''do''-''fa''-''sol'') ==== Triades appauvries ==== ; Accords fondés sur une tierce majeure ou mineure * accord de puissance : la tierce est omise, l'accord n'est constitué que de la fondamentale et de la quinte juste : 5 *: « ''do''<sup>5</sup> », « C<sup>5</sup> » pour l'accord de puissance de ''do'' (''do'' - ''la'') {{note|Très utilisé dans les musiques rock, hard rock et heavy metal, il est souvent joué renversé (''la'' - ''do'') ou bien avec l'ajout de l'octave (''do'' - ''la'' - ''do'').}} ==== Triades enrichies ==== ; Accords fondés sur une tierce majeure * accord de septième (la 7<sup>e</sup> est mineure) : 7 *: « ''do''<sup>7</sup> », « C<sup>7</sup> » pour l'accord de ''do'' septième, appelé « accord de septième de dominante de ''fa'' majeur » en musique classique (''do'' - ''mi'' - ''sol'' - ''si''♭) * accord de septième majeure : Δ, 7M ou maj7 *: « ''do'' <sup>Δ</sup> », « ''do'' <sup>maj7</sup> », « C<sup>Δ</sup> », « C<sup>7M</sup> »… pour l'accord de ''do'' septième majeure (''do'' - ''mi'' - ''sol'' - ''si'') ; Accords fondés sur une tierce mineure * accord de mineur septième (la tierce et la 7<sup>e</sup> sont mineures) : m7, min7 ou –7 *: « ''do'' m<sup>7</sup> », « ''do'' –<sup>7</sup> », « Cm<sup>7</sup> », « C–<sup>7</sup> »… pour l'accord de ''do'' mineur septième, appelé « accord de septième de dominante de ''fa'' mineur » en musique classique (''do'' - ''mi''♭ - ''sol'' - ''si''♭) * accord mineure septième majeure : m7M, m7maj, mΔ, –7M, –7maj, –Δ *: « ''do'' m<sup>7M</sup> », « ''do'' m<sup>maj7</sup> », « ''do'' –<sup>Δ</sup> », « Cm<sup>7M</sup> », « Cm<sup>maj7</sup> », « C–<sup>Δ</sup> »… pour l'accord de ''do'' mineur septième majeure (''do'' - ''mi''♭ - ''sol'' - ''si'') * accord de septième diminué (la quinte et la septième sont diminuée) : dim 7 ou o7 *: « ''do'' dim<sup>7</sup> », « ''do''<sup>o7</sup> », « Cdim<sup>7</sup> » ou « C<sup>o7</sup> » pour l'accord de ''do'' septième diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si''♭) * accord demi-diminué (seule la quinte est diminuée, la septième est mineure) : Ø ou –7(♭5) *: « ''do''<sup>Ø</sup> », « ''do''<sup>7(♭5)</sup> », « C<sup>Ø</sup> » ou « C<sup>7♭5</sup> » pour l'accord de ''do'' demi-diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si'') === Construction pythagoricienne des accords === Nous avons vu au débuts que lorsque l'on joue deux notes en même temps, leurs vibrations se superposent. Certaines superpositions créent un phénomène de battement désagréable, c'est le cas des secondes. Dans le cas d'une tierce majeure, les fréquences des notes quadruple et quintuple d'une même base : les fréquences s'écrivent 4׃<sub>0</sub> et 5׃<sub>0</sub>. Cette superposition de vibrations est agréable à l'oreille. Nous avons également vu que dans le cas d'une quinte juste, les fréquences sont le double et le triple d'une même base, ou encore le quadruple et sextuple si l'on considère la moitié de cette base. Ainsi, dans un accord parfait majeur, les fréquences des fondamentales des notes sont dans un rapport 4, 5, 6. De même, dans le cas d'un accord parfait mineur, les proportions sont de 1/6, 1/5 et 1/4. {{voir|[[../Caractéristiques_et_notation_des_sons_musicaux#Construction_pythagoricienne_et_gamme_de_sept_tons|Caractéristiques et notation des sons musicaux &gt; Construction pythagoricienne et gamme de sept tons]]}} === Un peu de physique : interférences === Les sons sont des vibrations. Lorsque l'on émet deux sons ou plus simultanément, les vibrations se superposent, on parle en physique « d'interférences ». Le modèle le plus simple pour décrire une vibration est la [[w:fr:Fonction sinus|fonction sinus]] : la pression de l'air P varie en fonction du temps ''t'' (en secondes, s), et l'on a pour un son « pur » : : P(''t'') ≈ sin(2π⋅ƒ⋅''t'') où ƒ est la fréquence (en hertz, Hz) du son. Si l'on émet deux sons de fréquence respective ƒ<sub>1</sub> et ƒ<sub>2</sub>, alors la pression vaut : : P(''t'') ≈ sin(2π⋅ƒ<sub>1</sub>⋅''t'') + sin(2π⋅ƒ<sub>2</sub>⋅''t''). Nous avons ici une [[w:fr:Identité trigonométrique#Transformation_de_sommes_en_produits,_ou_antilinéarisation|identité trigonométrique]] dite « antilinéarisation » : : <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{f_1 + f_2}{2}t \right ) \cdot \sin \left ( 2\pi \frac{f_1 - f_2}{2}t \right ).</math> On peut étudier simplement deux situations simples. [[Fichier:Battements interferentiels.png|vignette|Deux sons de fréquences proches créent des battements : la superposition d'une fréquence et d'une enveloppe.]] La première, c'est quand les fréquences ƒ<sub>1</sub> et ƒ<sub>2</sub> sont très proches. Alors, la moyenne (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2 est très proche de ƒ<sub>1</sub> et ƒ<sub>2</sub> ; et la demie différence (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2 est très proche de zéro. On a donc une enveloppe de fréquence très faible, (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2, dans laquelle s'inscrit un son de fréquence moyenne, (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2. C'est cette enveloppe de fréquence très faible qui crée les battements, désagréables à l'oreille. Sur l'image ci-contre, le premier trait rouge montre un instant où les vibrations sont opposées ; elles s'annulent, le son s'éteint. Le second trait rouge montre un instant où les vibrations sont en phase : elle s'ajoutent, le son est au plus fort. {{clear}} La seconde, c'est lorsque les deux fréquences sont des multiples entiers d'une même fréquence fondamentale ƒ<sub>0</sub> : ƒ<sub>1</sub> = ''n''<sub>1</sub>⋅ƒ<sub>0</sub> et ƒ<sub>0</sub> = ''n''<sub>0</sub>⋅ƒ<sub>0</sub>. On a alors : : <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{n_1 + n_2}{2}f_0 \cdot t \right ) \cdot \sin \left ( 2\pi \frac{n_1 - n_2}{2}f_0 \cdot t \right ).</math> On multiplie donc deux fonctions qui ont des fréquences multiples de ƒ<sub>0</sub>. La différence minimale entre ''n''<sub>1</sub> et ''n''<sub>2</sub> vaut 1 ; on a donc une enveloppe dont la fréquence est au minimum la moitié de ƒ<sub>0</sub>, c'est-à-dire un son une octave en dessous de ƒ<sub>0</sub>. Donc, cette enveloppe ne crée pas d'effet de battement, ou plutôt, le battement est trop rapide pour être perçu comme tel. Dans cette enveloppe, on a une fonction sinus dont la fréquence est également un multiple de ƒ<sub>0</sub> ; l'enveloppe et la fonction qui y est inscrite ont donc de nombreux « points communs », d'où l'effet harmonieux. === Le tonnetz === [[File:Speculum musicae.png|thumb|right|225px|Euler, ''De harmoniæ veris principiis'', 1774, p. 350.]] En allemand, le terme ''Tonnetz'' (se prononce « tône-netz ») signifie « réseau tonal ». C'est une représentation graphique des notes qui a été imaginée par [[w:Leonhard Euler|Leonhard Euler]] en 1739. Cette représentation graphique peut aider à la mémorisation de certains concepts de l'harmonie. Cependant, son application est très limitée : elle ne concerne que l'intonation juste d'une part, et que les accords parfait des tonalités majeures et mineures naturelles d'autre part. La représentation contenant les douze notes de la musique savante occidentale, on peut bien sûr représenter d'autres objets, comme les accords de septième ou les accords diminués, mais la représentation graphique est alors compliquée et perd son intérêt pédagogique. On part d'une note, par exemple le ''do''. Si on progresse vers la droite, on monte d'une quinte juste, donc ''sol'' ; vers la gauche, on descend d'une quinte juste, donc ''fa''. Si on va vers le bas, on monte d'une tierce majeure, donc ''mi'' ; si on va vers le haut, on descend d'une tierce majeure, donc ''la''♭ ou ''sol''♯ fa — do — sol — ré | | | | la — mi — si — fa♯ | | | | do♯ — sol♯ — ré♯ — si♭ La figure forme donc un filet, un réseau. On voit que ce réseau « boucle » : si on descend depuis le ''do''♯, on monte d'une tierce majeure, on obtient un ''mi''♯ qui est l'enharmonique du ''fa'' qui est en haut de la colonne. Si on va vers la droite à partir du ''ré'', on obtient le ''la'' qui est au début de la ligne suivante. Si on ajoute des diagonales allant vers la droite et le haut « / », on met en évidence des tierces mineures : ''la'' - ''do'', ''mi'' - ''sol'', ''si'' - ''ré'', ''do''♯ - ''mi''… fa — do — sol — ré | / | / | / | la — mi — si — fa♯ | / | / | / | do♯ — sol♯ — ré♯ — si♭ Donc les liens représentent : * | : tierce majeure ; * — : quinte juste ; * / : tierce mineure. [[Fichier:Tonnetz carre accords fr.svg|thumb|Tonnetz avec les accords parfaits. Les notes sont en notation italienne et les accords en notation jazz.]] On met ainsi en évidence des triangles dont un côté est une quinte juste, un côté une tierce majeure et un côté une tierce mineure ; c'est-à-dire que les notes aux sommets du triangle forment un accord parfait majeur (par exemple ''do'' - ''mi'' - ''sol'') : <div style="font-family:courier; background-color:#fafafa"> fa&nbsp;&nbsp;—&nbsp;'''do&nbsp;&nbsp;&nbsp;—&nbsp;sol'''&nbsp;—&nbsp;ré<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;'''|&nbsp;&nbsp;&nbsp;/'''&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;|<br /> la&nbsp;&nbsp;—&nbsp;'''mi'''&nbsp;&nbsp;&nbsp;—&nbsp;si&nbsp;&nbsp;—&nbsp;fa♯<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;/&nbsp;&nbsp;|<br /> do♯&nbsp;—&nbsp;sol♯&nbsp;—&nbsp;ré♯&nbsp;—&nbsp;si♭ </div> ou un accord parfait mineur (''la'' - ''do'' - ''mi''). <div style="font-family:courier; background-color:#fafafa"> fa&nbsp;&nbsp;—&nbsp;'''do'''&nbsp;&nbsp;&nbsp;—&nbsp;sol&nbsp;—&nbsp;ré<br /> |&nbsp;&nbsp;'''/&nbsp;&nbsp;|'''&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;|<br /> '''la&nbsp;&nbsp;—&nbsp;mi'''&nbsp;&nbsp;&nbsp;—&nbsp;si&nbsp;&nbsp;—&nbsp;fa♯<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;/&nbsp;&nbsp;|<br /> do♯&nbsp;—&nbsp;sol♯&nbsp;—&nbsp;ré♯&nbsp;—&nbsp;si♭ </div> Un triangle représente donc un accord, et un sommet représente une note. Si on passe d'un triangle à un triangle voisin, alors on passe d'un accord à un autre accord, les deux accords ayant deux notes en commun. Ceci illustre la notion de « plus court chemin » en harmonie : si on passe d'un accord à un autre en gardant un côté commun, alors on a un mouvement conjoint sur une seule des trois voix. Par rapport à l'harmonie fonctionnelle : les accords sont contigus à leur fonction, par exemple en ''do'' majeur : * fonction de tonique ({{Times New Roman|I}}) : C, A– et E– sont contigus ; * fonction de sous-dominante ({{Times New Roman|IV}}) : F et D– sont contigus ; * fonction de dominante ({{Times New Roman|V}}) : G et B<sup>o</sup> sont contigus. On notera que les triangles d'un schéma ''tonnetz'' ne représentent que des accords parfaits. Pour représenter un accord de quinte diminuée (''si'' - ''ré'' - ''fa'') ou les accords de septième, en particulier l'accord de septième de dominante, il faut étendre le ''tonnetz'' et l'on obtient des figures différentes. Par ailleurs, il est adapté à ce que l'on appelle « l'intonation juste », puisque tous les intervalles sont idéaux. [[Fichier:Tonnetz carre accords etendu fr.svg|vignette|Tonnetz étendu.]] [[Fichier:Tonnetz carre do majeur accords fr.svg|vignette|Tonnetz de la tonalité de ''do'' majeur. La représentation de l'accord de quinte diminuée sur ''si'' (B<sup>o</sup>) est une ligne et non un triangle.]] [[Fichier:Tonnetz carre do mineur accords fr.svg|vignette|Tonnetz des tonalités de ''do'' mineur naturel (haut) et ''do'' mineur harmonique (bas).]] Si l'on étend un peu le réseau : ré♭ — la♭ — mi♭ — si♭ — fa | / | / | / | / | fa — do — sol — ré — la | / | / | / | / | la — mi — si — fa♯ — do♯ | / | / | / | / | do♯ — sol♯ — ré♯ — la♯ — mi♯ | / | / | / | / | mi♯ — do — sol — ré — la on peut donc trouver des chemins permettant de représenter les accords de septième de dominante (par exemple en ''do'' majeur, G<sup>7</sup>) fa / sol — ré | / si et des accords de quinte diminuée (en ''do'' majeur : B<sup>o</sup>) fa / ré / si Une gamme majeure ou mineure naturelle peut se représenter par un trapèze rectangle : ''do'' majeur fa — do — sol — ré | / la — mi — si et ''do'' mineur la♭ — mi♭ — si♭ / | fa — do — sol — ré En revanche, la représentation d'une tonalité nécessite d'étendre le réseau afin de pouvoir faire figurer tous les accords, deux notes sont représentées deux fois. La représentation des tonalités mineures harmoniques prend une forme biscornue, ce qui nuit à l'intérêt pédagogique de la représentation. [[Fichier:Neo-Riemannian Tonnetz.svg|vignette|upright=2|Tonnetz avec des triangles équilatéraux.]] On peut réorganiser le schéma en décalant les lignes, afin d'avoir des triangles équilatéraux. Sur la figure ci-contre (en notation anglo-saxonne) : * si on monte en allant vers la droite « / », on a une tierce mineure ; * si on descend en allant vers la droite « \ », on a une tierce majeure ; * les liens horizontaux « — » représentent toujours des quintes justes * les triangles pointe en haut sont des accords parfaits mineurs ; * les triangles pointe en bas sont des accords parfaits majeurs. On a alors les accords de septième de dominante F / G — D \ / B et de quinte diminuée F / D / B les tonalités majeures F — C — G — D \ / A — E — B et les tonalités mineures naturelles A♭ — E♭ — B♭ / \ F — C — G — D == Notes et références == {{références}} == Voir aussi == === Bibliographie === * {{ouvrage |prénom1=Francesco |nom1=Gasparini |titre=L'Armonico Pratico al Cimbalo |éditeur=Giusseppe Antonio Silvani |lieu=Bologne |année=1772 |lang=it |url=https://imslp.org/wiki/L'Armonico_Pratico_al_Cimbalo_(Gasparini%2C_Francesco)}} === Liens externes === {{wikipédia|Consonance (harmonie tonale)}} {{wikipédia|Disposition de l'accord}} {{wikisource|Petit Manuel d’harmonie}} * {{lien web | url = https://www.apprendrelesolfege.com/chiffrage-d-accords | titre = Chiffrage d'accords (classique) | site = Apprendrelesolfege.com | consulté le = 2020-12-03 }} * {{lien web | url = https://www.coursd-harmonie.fr/introduction/introduction2_le_chiffrage_d_accords.php | titre = Introduction II : Le chiffrage d'accords | site = Cours d'harmonie.fr | consulté le = 2021-12-14 }} * {{lien web | url=https://www.coursd-harmonie.fr/ | titre = Cours d'harmonie en ligne | auteur = Jean-Baptiste Voinet | site=coursd-harmonie.fr | consulté le = 2021-12-20 }} * {{lien web | url=http://e-harmonie.e-monsite.com/ | titre = Cours d'harmonie classique en ligne | auteur = Olivier Miquel | site=e-harmonie | consulté le = 2021-12-24 }} * {{lien web | url=https://fr.audiofanzine.com/theorie-musicale/editorial/dossiers/les-gammes-et-les-modes.html | titre = Les bases de l’harmonie | site = AudioFanzine | date = 2013-07-23 | consulté le = 2024-01-12 }} * {{lien web | url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/l-accord-de-tristan-dechiffre-et-desacralise-2481605 | titre = L’Accord de Tristan déchiffré et désacralisé ! | site = France Musique | date = 2025-10-22 | consulté le = 2025-10-23 }} * {{lien web | url=https://www.youtube.com/watch?v=X62SAnyfQYw | titre = Qu'est-ce que la basse continue dans la musique baroque ? | auteur = Clément Rochefort | site = YouTube/France Musique | date = 2022-05-02 | consulté le = 2026-08-04 }} ---- ''[[../Mélodie|Mélodie]]'' &lt; [[../|↑]] &gt; ''[[../Représentation musicale|Représentation musicale]]'' [[Catégorie:Formation musicale (livre)|Harmonie]] 459wr5vps6qrmxfbo6wk7uqyki7hrpy 773912 773911 2026-10-02T08:57:24Z Cdang 1202 /* Notation des accords de trois notes */ obli 773912 wikitext text/x-wiki {{Bases de solfège}} <span style="font-size:25px;">6. Harmonie</span> L'harmonie désigne les notes jouées en même temps, soit plusieurs instruments jouant chacun une note, soit un instrument jouant un accord (instrument dit polyphonique). == Première approche == L'exemple le plus simple d'harmonie est sans doute la chanson en canon : c'est un chant polyphonique, c'est-à-dire à plusieurs voix, chaque voix chantant la même chose en décalé. Prenons par exemple ''Vent frais, vent du matin'' (la version originale est ''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609) : [[Fichier:Vent frais vent du matin.svg|class=transparent|center|Partition de ''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]] [[Fichier:Vent frais vent du matin.midi|vignette|''Vent frais, vent du matin'' (''{{lang|en|Hey, Ho Nobody at Home}}'' de Thomas Ravenscroft, 1609).]] nous voyons que les voix se superposent de manière « harmonieuse ». Les notes de chaque voix se correspondent point par point (avec un retard), c'est donc un type d'harmonie polyphonique appelé « contrepoint ». Considérons la première note de la mesure 6 pour chaque voix. Nous avons la superposition des notes ''ré''-''fa''-''la'' (du grave vers l'aigu) ; la superposition de notes jouées ou chantées ensembles s'appelle un accord. Cet accord ''ré''-''fa''-''la'' porte le nom « d'accord parfait de ''ré'' mineur » : * « ''ré'' » car la note fondamentale est un ''ré'' ; * « parfait » car il est l'association d'une tierce, ''ré''-''fa'', et d'une quinte juste, ''ré''-''la'' ; * « mineur » car le premier intervalle, ''ré''-''fa'', est une tierce mineure. Considérons maintenant un chant accompagné au piano. Le piano peut jouer plusieurs notes en même temps, il peut jouer des accords. [[Fichier:Au clair de le lune chant et piano.svg|class=transparent|center|Deux premières mesure d’Au clair de la lune.]] [[Fichier:Au clair de le lune chant et piano.midi|vignette|Deux premières mesure d’Au clair de la lune.]] L'accord, les notes à jouer simultanément, sont écrites « en colonne ». Lorsqu'on les énonce, on les lit de bas en haut mais le pianiste les joue en pressant les touches du clavier en même temps, de manière « plaquée ». Le premier accord est composé des notes ''do''-''mi''-''sol'' ; il est appelé « accord parfait de ''do'' majeur » car la note fondamentale est ''do'', qu'il est l'association d'une tierce et d'une quinte juste et que le premier intervalle, ''do''-''mi'', est une tierce majeure. Si l'on lit toutes les notes, de la plus grave à la plus aiguë, nous avons : ''do'' (à la clef de ''fa'', main gauche du piano) - ''do'' - ''mi'' - ''sol'' (main droite du piano) - ''do'' (chant) ; nous voyons que la note ''do'' est jouée plusieurs fois, cela ne change pas la qualité de l'accord. == Consonance et dissonance == [[Fichier:Seconde majeure septieme mineure.svg|vignette|Seconde majeure et son renversement, le septième mineure.]] Les notions de consonance et de dissonance sont culturelles et changent selon l'époque. Nous pouvons néanmoins noter que : * l'accord de seconde, et son renversement la septième, créent des battements, les notes « frottent », c'est un intervalle harmonique dissonant ; mais dans le cas de la septième, comme les notes sont éloignées, le frottement est moins perceptible ; * les accords d'unisson, de tierce, quarte et quinte sonnent agréablement à l'oreille, ils sont consonants. Dans la musique savante européenne, au début du Moyen-Âge, seuls les accords d'unisson, de quarte et de quinte étaient considérés comme consonants, d'où leur qualification de « juste ». La tierce, et son renversement la sixte, étaient perçues comme dissonantes ; à partir de la Renaissance, ces deux intervalles sont considérés comme consonants. L'harmonie joue avec les consonances et les dissonances. Dans un premier temps, les harmonies dissonantes sont utilisées pour créer des tensions qui sont ensuite résolues, on utilise des successions « consonant-dissonant-consonant ». À force d'entendre des intervalles considérés comme dissonants, l'oreille s'habitue et certains finissent par être considérés comme consonants ; c'est ce qui est arrivé à la tierce et à la sixte à la fin du Moyen Âge avec le contrepoint, puis à la septième plus tardivement. Il faut ici aborder la notion d'harmonique des notes. [[File:Harmoniques de do.svg|thumb|Les six premières harmoniques de ''do''.]] Lorsque l'on joue une note, on entend d'autres notes plus aigües et plus faibles ; la note jouée est appelée la « fondamentale » et les notes plus aigües et plus faibles sont les « harmoniques ». C'est cette accumulation d'harmoniques qui donne la couleur au son, son timbre, qui fait qu'un piano ne sonne pas comme un violon. Par exemple, si l'on joue un ''do''<sup>1</sup><ref>Pour la notation des octaves, voir ''[[../Représentation_musicale#Désignation_des_octaves|Représentation musicale &gt; Désignation des octaves]]''.</ref> (fondamentale), on entend le ''do''<sup>2</sup> (une octave plus aigu), puis un ''sol''<sup>2</sup>, puis encore un ''do''<sup>3</sup> plus aigu, puis un ''mi''<sup>3</sup>, puis encore un ''sol''<sup>3</sup>, puis un ''si''♭<sup>3</sup>… Ainsi, puisque lorsque l'on joue un ''do'' on entend aussi un ''sol'' très léger, alors jouer un ''do'' et un ''sol'' simultanément n'est pas choquant. De même pour ''do'' et ''mi''. De là vient la notion de consonance. Le statut du ''si''♭ est plus ambigu. Il fait partie des harmoniques qui sonnent naturellement, mais il forme une seconde descendante avec le ''do'', intervalle dissonant. Par ailleurs, on remarque que le ''si''♭ ne fait pas partie de la gamme de ''do'' majeur, contrairement au ''sol'' et au ''mi''. Pour le jeu sur les dissonances, on peut écouter par exemple la ''Toccata'' en ''ré'' mineur, op. 11 de Sergueï Prokofiev (1912). : {{lien web |url=https://www.youtube.com/watch?v=AVpnr8dI_50 |titre=Yuja Wang Prokofiev Toccata |site=YouTube |date=2019-02-26 |consulté le=2021-12-19}} == Contrepoint == Dans le chant grégorien, la notion d'accord n'existe pas. L'harmonie provient de la superposition de plusieurs mélodies, notamment dans ce que l'on appelle le « contrepoint ». Le terme provient du latin ''« punctum contra punctum »'', littéralement « point par point », et désigne le fait que les notes de chaque voix se correspondent. L'exemple le plus connu de contrepoint est le canon, comme ''Frère Jacques'' ou bien ''Vent frais, vent du matin'' présenté ci-dessus : chaque note d'un couplet correspond à une note du couplet précédent. Certains morceaux sont bâtis sur une écriture « en miroir » : l'ordre des notes est inversé entre les deux voix, ou bien les intervalles sont inversés (« mouvement contraire » : une tierce montante sur une voix correspond à une tierce descendante sur l'autre). On peut également citer le « mouvement oblique » (une des voix, le bourdon, chante toujours la même note) et le mouvement parallèle (les deux voix chantent le même air mais transposé, l'une est plus aiguë que l'autre). Nous reproduisons ci-dessous le début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel. [[Fichier:Haendel Sonate en trio re mineur debut canon.svg | vignette | center | upright=2 | Début du second ''Allergo'' de la sonate en trio en ''ré'' mineur de Haendel.]] [[Fichier:Haendel Sonate en trio re mineur debut.midi | vignette | Début du second ''Allegro'' de la sonate en trio en ''ré'' mineur de Haendel.]] Nous avons mis en évidence la construction en canon avec des encadrés de couleur : sur les quatre premières mesures, nous voyons trois motifs repris alternativement par une voix et par l'autre. Ce type de procédé est très courant dans la musique baroque. Les procédés du contrepoint s'appliquent également à la danse : * unisson : les danseurs et danseuses font les mêmes gestes en même temps ; * répétition : le fait de répéter une série de gestes, une « phrase dansante » ; * canon : les gestes sont faits avec un décalage régulier d'un danseur ou d'une danseuse à l'autre ; * cascade : forme de canon dans laquelle le décalage est très petit ; * contraste : deux danseur·euses, ou deux groupes, ont des gestuelles très différentes ; * accumulation : la gestuelle se complexifie par l'ajout d'éléments au fur et à mesure ; ou bien le nombre de danseur·euses augmente ; * dialogue : les gestes de danseur·euses ou de groupes se répondent ; * contre-point : la gestuelle d'un ou une danseuse se superpose à la gestuelle d'un groupe ; * lâcher-rattraper : les danseurs et danseuses alternent danse à l'unisson et gestuelles indépendantes. : {{lien web | url=https://www.youtube.com/watch?v=wgblAOzedFc | titre=Les procédés de composition en danse | auteur= Doisneau Sport TV | site=YouTube | date=2020-03-16 | consulté le=2021-01-21 }} {{...}} == Ostinato == L'ostinato, mot italien signifiant « obstiné », c'est le fait de jouer en boucle un passage. L'ostinato peut ainsi former une structure autour de laquelle peut se développer le morceau. Lorsque l'ostinato est joué par l'instrument de basse (basson, violoncelle, viole de gambe basse, flûte à bec basse, main gauche du piano…), on parle de basse obstinée. Les grilles d'accord en jazz sont des ostinatos (ou ostinati) joués par la section rythmique. Exemples : * {{lien web |url=https://www.youtube.com/watch?v=QZpGe5rNJkI&t=41s |titre=Epic Patty Cake Song (I'll Think Of You) |auteur= Kurt Hugo Schneider |site=YouTube |date=2014-08-12 |consulté le=2026-01-24}} : un ostinato de percussions corporelles ; * {{lien web |url=https://www.youtube.com/watch?v=Kv-37-nXC48 |titre=Pow Wow ''Le lion est mort ce soir'' Les Victoires de la Musique 1993 |auteur=Solomon Linda |date=2015-02-02 |consulté le=2026-01-24}} : dans ce chant ''a capella'' (sans accompagnement d'instrument), les voix de basse forment une basse obstinée, sur les syllabes « oëap » durant le couplet et « awimbowé » sur le refrain ; * {{lien web |url=https://www.youtube.com/watch?v=JvNQLJ1_HQ0 |titre=Pachelbel Canon in D Major - the original and best version. |auteur=Voices of Music |site=YouTube |date=2008-09-02 |consulté le=2026-01-24}} : le ''Canon de Pachelbel de ''ré'' mineur'' (''Canon per tre Violini e Basso'', Johann Pachelbel, vers 1680) est construit sur une basse obstinée de huit mesures. == Les accords en général == Initialement, on a des chants polyphoniques, des voix qui chantent chacune une mélodie, les mélodies se mêlant. On remarque que certaines superpositions de notes sonnent de manière plus ou moins agréables, consonantes ou dissonantes. On en vient alors à associer ces notes, c'est-à-dire à considérer dès le départ la superposition de ces notes et non pas la rencontre de ces notes au gré des mélodies. Ces groupes de notes superposées forment les accords. En Europe, cette notion apparaît vers le {{pc|xiv}}<sup>e</sup> siècle avec notamment la ''[[wikipedia:fr:Messe de Notre Dame|Messe de Notre Dame]]'' de Guillaume de Machaut (vers 1360-1365). À l'origine, dans les chants polyphoniques, les voix sont d'égale importance ; pour un chant à trois voix, les voix sont appelées, de la plus grave à la plus aigüe, « basse », « taille » (ou ténor) et « dessus » (ou soprano). Puis, au cours du temps, une des voix devient plus importante, le « cantus firmus » ; initialement, il s'agit de la voix intermédiaire, la taille (ténor), puis c'est la voix la plus aigüe, le dessus (soprano). À la Renaissance, au {{pc|xvi}}{{e}} siècle, apparaît la notion de « monodie accompagnée » : la voix principale, le cantus firmus, devient la voix mélodique, la monodie, et les autres voix servent à l'accompagner. À l'époque baroque, au {{pc|xvii}}{{e}} siècle, apparaît la notion d'accord, notes sonnant simultanément, avec une notation simplifiée, la « basse chiffrée » : a lieu d'écrire toutes les notes de l'accord, on écrit la basse et des chiffres indiquent quelles sont les autres notes à jouer. La notion « d'accord parfait » est consacrée par [[wikipedia:fr:Jean-Philippe Rameau|Jean-Philippe Rameau]] dans son ''Traité de l'harmonie réduite à ses principes naturels'', publié en 1722. === Qu'est-ce qu'un accord ? === Un accord est un ensemble d'au minimum trois notes jouées en même temps. « Jouées » signifie qu'il faut qu'à un moment donné, elles sonnent en même temps, mais le début ou la fin des notes peut être à des instants différents. Considérons que l'on joue les notes ''do'', ''mi'' et ''sol'' en même temps. Cet accord s'appelle « accord de ''do'' majeur ». En musique classique, on lui adjoint l'adjectif « parfait » : « accord parfait de ''do'' majeur ». Nous représentons ci-dessous trois manière de faire l'accord : avec trois instruments jouant chacun une note : [[Fichier:Do majeur trois portees.svg|class=transparent|center|Accord de ''do'' majeur avec trois instruments différents.]] Avec un seul instrument jouant simultanément les trois notes : [[Fichier:Chord C.svg|class=transparent|center|Accord de ''do'' majeur joué par un seul instrument.]] L'accord tel qu'il est joué habituellement par une guitare d'accompagnement : [[Fichier:Do majeur guitare.svg|class=transparent|center|Accord de ''do'' majeur à la guitare.]] Pour ce dernier, nous représentons le diagramme indiquant la position des doigts sur le manche au dessus de la portée et la tablature en dessous. Ici, c'est au total six notes qui sont jouées : ''mi'' grave, ''do'' médium, ''mi'' médium, ''sol'' médium, ''do'' aigu, ''mi'' aigu. Mais il s'agit bien des trois notes ''do'', ''mi'' et ''sol'' jouées à des octaves différentes. Nous remarquons également que la note de basse (la note la plus grave), ''mi'', est différente de la note fondamentale (celle qui donne le nom à l'accord), ''do'' ; l'accord est dit « renversé » (voir plus loin). === Comment joue-t-on un accord ? === Les notes ne sont pas forcément jouées en même temps ; elles peuvent être « égrainées », jouée successivement, ce que l'on appelle un arpège. La partition ci-dessous montre six manières différentes de jouer un accord de ''la'' mineur à la guitare, plaqué puis arpégé. [[Fichier:La mineur differentes executions.svg|class=transparent|center|Différentes exécution de l'accord de do majeur à la guitare.]] [[Fichier:La mineur differentes executions midi.midi|vignette|Différentes exécution de l'accord de la mineur à la guitare.]] Vous pouvez écouter l'exécution de cette partition avec le lecteur ci-contre. Seuls les instruments polyphoniques peuvent jouer les accords plaqués : instruments à clavier (clavecin, orgue, piano, accordéon), les instruments à plusieurs cordes pincées (harpe, guitare ; violon, alto, violoncelle et contrebasse joués en pizzicati). Les instruments à corde frottés de la famille du violon peuvent jouer des notes par deux à l'archet mais pas plus du fait de la forme bombée du chevalet ; cependant, un mouvement rapide permet de jouer les quatre cordes de manière très rapprochée. Les instruments à percussion de type xylophone ou le tympanon permettent de jouer jusqu'à quatre notes simultanément en tenant deux baguettes (mailloches, maillets) par main. Tous les instruments peuvent jouer des arpèges même si, dans le cas des instruments monodiques, les notes ne continuent pas à sonner lorsque l'on passe à la note suivante. L'arpège peut être joué par l'instrument de basse (basson, violoncelle, contrebasse, guitare basse, pédalier de l'orgue…), notamment dans le cas d'une basse continue ou d'une ''{{lang|en|walking bass}}'' (« basse marchante » : la basse joue des noires, donnant ainsi l'impression qu'elle marche). En jazz, et spécifiquement au piano, on a recours au ''{{lang|en|voicing}}'' : on choisit la manière dont on organise les notes pour donner une couleur spécifique, ou bien pour créer une mélodie en enchaînant les accords. Il est fréquent de ne pas jouer toutes les notes : si on n'en garde que deux, ce sont la tierce et la septième, car ce sont celles qui caractérisent l'accord (selon que la tierce est mineure ou majeure, que la septième est majeure ou mineure), et la fondamentale est en général jouée par la contrebasse ou guitare basse. {{clear}} === Classes d'accord === [[Fichier:Intervalles harmoniques accords classes.svg|vignette|upright=1.5|Intervalles harmoniques dans les accords classés de trois, quatre et cinq notes.]] Un accord composé d'empilement de tierces est appelé « accord classé ». En musique tonale, c'est-à-dire la musique fondée sur les gammes majeures ou mineures (cas majoritaire en musique classique), on distingue trois classes d'accords : * les accords de trois notes, ou triades, ou accords de quinte ; * les accords de quatre notes, ou accords de septième ; * les accords de cinq notes, ou accords de neuvième. En empilant des tierces, si l'on part de la note fondamentale, on a donc de intervalles de tierce, quinte, septième et neuvième. En musique tonale, les accords avec d'autres intervalles (hors renversement, voir ci-après), typiquement seconde, quarte ou sixte, sont considérés comme des transitions entre deux accords classés. Ils sont appelés, selon leur utilisation, « accords à retard » (en anglais : ''{{lang|en|suspended chord}}'', accord suspendu) ou « appoggiature » (note « appuyée », étrangère à l'harmonie). Voir aussi plus loin la notion de note étrangère. === Renversements d'accords === [[File:Accord do majeur renversements.svg|thumb|Accord parfait de do majeur et ses renversements.]] [[Fichier:Progression dominante renverse parfait do majeur.svg|vignette|upright=0.6|Progression accord de dominante renversé → accord parfait en ''do'' majeur.]] Un accord classé est donc un empilement de tierces. Si l'on change l'ordre des notes, on a toujours le même accord mais il est fait avec d'autres intervalles harmoniques. Par exemple, l'accord parfait de ''do'' majeur dans son état fondamental, c'est-à-dire non renversé, s'écrit ''do'' - ''mi'' - ''sol''. Sa note fondamentale, ''do'', est aussi se note de basse. Si maintenant on prend le ''do'' de l'octave supérieure, l'accord devient ''mi - sol - do'' ; c'est l'empilement d'une tierce ''(mi - sol)'' et d'une quarte ''(sol - do)'', soit la superposition d'une tierce ''(mi - sol)'' et d'une sixième ''(mi - do)''. C'est le premier renversement de l'accord parfait de ''do'' majeur ; la fondamentale est toujours ''do'' mais la basse est ''mi''. Le second renversement est ''sol - do - mi''. L'utilisation de renversement peut faciliter l'exécution de la progression d'accord. Par exemple, en tonalité ''do'' majeur, si l'on veut passer de l'accord de dominante ''sol - si - ré'' à l'accord parfait ''do - mi - sol'', alors on peut utiliser le second renversement de l'accord de dominante : ''ré - sol - si'' → ''do - mi - sol''. Ainsi, la basse descend juste d'un ton ''(ré → do)'' et sur un piano, la main reste globalement dans la même position. Le renversement d'un accord permet également de respecter certaines règles de l'harmonie classique, notamment éviter que des voix se suivent strictement (« mouvement parallèle »), ce qui aurait un effet de platitude. De manière générale, la notion de renversement permet deux choses : * d'enrichir l'œuvre : pour créer une harmonie donnée (c'est-à-dire des sons sonnant bien ensemble), nous avons plus de souplesse, nous pouvons organiser ces notes comme nous le voulons selon les voix ; * de simplifier l'analyse : quelle que soit la manière dont sont organisées les notes, cela nous ramène à un même accord. {{citation bloc|Or il, y a plusieurs manières de jouer un accord, selon que l'on aborde par la première note qui le constitue, ''do mi sol'', la deuxième, ''mi sol do'', ou la troisième note, ''sol do mi''. Ce sont les renversements, [que Rameau] va classer en différentes combinaisons d'une seule matrice. Faisant cela, Rameau divise le nombre d'accords [de septième] par quatre. Il simplifie, il structure […].|{{ouvrage|prénom1=André |nom1=Manoukian |titre=Sur les routes de la musique |éditeur=Harper Collins |année=2021 |passage=54 |isbn=979-1-03391201-9}} }} {{clear}} [[File:Plusieurs realisation 1er renversement doM.svg|thumb|Plusieurs réalisation du premier renversement de l'accord de ''do'' majeur.]] Notez que # Les notes peuvent être répétées plusieurs fois, à différentes octaves. # Dans la notion de renversement, seule importe en fait la note de basse. Ainsi, les accords ''mi-sol-do'', ''mi-do-sol'', ''mi-do-mi-sol'', ''mi-sol-mi-do''… sont tous une déclinaison du premier renversement de ''do-mi-sol'' et ils seront abrégés de la même manière (''mi''<sup>6</sup> en musique classique ou C/E en musique populaire et jazz, voir plus bas). {{clear}} == Notation des accords de trois notes == Les accords de trois notes sont appelés « accords de quinte » en classique, et « triades » en jazz. [[Fichier:Progression dominante renverse parfait do majeur chiffrage.svg|vignette|upright=0.7|Chiffrage du second renversement d'un accord de ''sol'' majeur et d'un accord de ''do'' majeur : notation en musique populaire et jazz (haut) et notation de basse chiffrée (bas).]] Les accords sont construits de manière systématique. Nous pouvons donc les représenter de manière simplifiée. Cette notation simplifiée des accords est appelée « chiffrage ». Reprenons la progression d'accords ci-dessus : « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » dans la tonalité de ''do'' majeur. On utilise en général trois notations différentes : * en musique populaire, jazz, rock… un accord est désigné par sa note fondamentale ; ici donc, les accords sont notés « ''sol'' - ''do'' » ou, en notation anglo-saxonne, « G - C » ;<br /> comme le premier accord est renversé, on indique la note de basse après une barre, la progression d'accords est donc chiffrée '''« ''sol''/''ré'' - ''do'' »''' ou '''« G/D - C »''' ;<br /> il s'agit ici d'accords composés d'une tierce majeure et d'une quinte juste ; si les accords sont constitués d'intervalles différents, nous ajoutons un symbole après la note : « m » ou « – » si la tierce est mineure, « dim » ou « ° » si la quinte est diminuée ; * en musique classique, on utilise la notation de « basse chiffrée » (utilisée notamment pour noter la basse continue en musique baroque) : on indique la note de basse sur la portée et on lui adjoint l'intervalle de la fondamentale à la note la plus haute (donc ici respectivement 6 et 5, puisque ''sol''-''si'' est une sixte et ''do''-''sol'' est une quinte), étant sous-entendu que l'on a des empilements de tierce en dessous ; mais dans le cas du premier accord, le premier intervalle n'est pas une tierce, mais une quarte ''(ré''-''sol)'', on note donc '''« ''ré'' <sup>6</sup><sub>4</sub> - ''do'' <sup>5</sup> »'''<ref>quand on ne dispose pas de la notation en supérieur (exposant) et inférieur (indice), on utilise parfois une notation sous forme de fraction : ''sol'' 6/4 et ''do'' 5/.</ref> ; * lorsque l'on fait l'analyse d'un morceau, on s'attache à identifier la note fondamentale de l'accord (qui est différente de la basse dans le cas d'un renversement) ; on indique alors le degré de la fondamentale : '''« {{Times New Roman|V<sup>6</sup><sub>4</sub> - I<sup>5</sup>}} »'''. La notation de basse chiffrée permet de construire l'accord à la volée : * on joue la note indiquée (basse) ; * s'il n'y a pas de 2 ni de 4, on lui ajoute la tierce ; * on ajoute les intervalles indiqués par le chiffrage. La notation de musique jazz oblige à connaître la composition des différents accords, mais une fois que ceux-ci sont acquis, il n'y a pas besoin de reconstruire l'accord. La notation de basse chiffrée avec les chiffres romains n'est pas utilisée pour jouer, mais uniquement pour analyser ; Sur les partitions avec basse chiffrée, il y a simplement les chiffrages indiqués au-dessus de la partie de basse. Le chiffrage avec le degré en chiffres romains présente l'avantage d'être indépendant de la tonalité et donc de se concentrer sur la fonction de l'accord au sein de la tonalité. Par exemple, ci-dessous, nous pouvons parler de la progression d'accords « {{Times New Roman|V - I}} » de manière générale, cette notation étant valable quelle que soit la tonalité. [[File:Progression dominante renverse parfait do majeur chiffrage basse continue.svg|thumb|Chiffrage en notation basse chiffrée de la progression d'accords « second renversement de l'accord de dominante - accord sur la tonique à l'état fondamental » en do majeur.]] {{note|En notation de base continue avec fondamentale en chiffres romains, la fondamentale est toujours indiquée ''sous'' la portée de la partie de basse. Les intervalles sont indiqués au-dessus de la portée de la partie de basse ; lorsque l'on fait une analyse, on peut ayssi les indiquer à côté du degré en chiffres romains, donc sous la portée de la basse.}} {{note|En notation rock, le 5 en exposant indique un accord incomplet avec uniquement la fondamentale et la quinte, un accord sans tierce appelé « accord de puissance » ou ''{{lang|en|power chord}}''. Par exemple, C<sup>5</sup> est l'accord ''do-sol''.}} {{clear}} [[Fichier:Accords parfait do majeur basse chiffree fondamental et renverse.svg|vignette|upright=2.5|Chiffrage de l'accord parfait de ''do'' majeur en basse chiffrée, à l'état fondamental et ses renversements.]] Concernant les accords parfaits en notation de basse chiffrée : * un accord parfait à l'état fondamental est chiffré « <sup>5</sup> » ; on l'appelle « accord de quinte » ; * le premier renversement est chiffré « <sup>6</sup> » (la tierce est implicite) ; on l'appelle « accord de sixte » ; * le second renversement est noté « <sup>6</sup><sub>4</sub> » ; on l'appelle « accord de sixte et de quarte » (ou bien « de quarte et de sixte »). Par exemple, pour l'accord parfait de ''do'' majeur : * l'état fondamental ''do''-''mi''-''sol'' est noté ''do''<sup>5</sup> (« ''do'' chiffré cinq ») ; * le premier renversement ''mi''-''sol''-''do'' est noté ''mi''<sup>6</sup> (« ''mi'' chiffré six ») ; * le second renversement ''sol''-''do''-''mi'' est noté ''sol''<sup>6</sup><sub>4</sub> (« ''sol'' chiffré six, quatre »). Il y a une exception : l'accord construit sur la sensible (7{{e}} degré) contient une quinte diminuée et non une quinte juste. Le chiffrage est donc différent : * l'état fondamental ''si''-''ré''-''fa'' est noté ''si''<sup><s>5</s></sup> (« ''si'' chiffré cinq barré »), « accord de quinte diminuée » ; * le premier renversement ''ré''-''fa''-''si'' est noté ''ré''<sup>+6</sup><sub>3</sub> (« ''ré'' chiffré plus six, trois »), « accord de sixte sensible et tierce » ; * le second renversement ''fa''-''si''-''ré'' est noté ''fa''<sup>6</sup><sub>+4</sub> (« ''fa'' chiffré si, plus quatre »), « accord de sixte et quarte sensible ». Par ailleurs, on ne considère pas qu'il est fondé sur la sensible, mais sur la dominante. Si certains indiquent le degré {{Times New Roman|VII}}, d'autres indiquent le degré {{Times New Roman|V}} en mettant des guillemets, « {{Times New Roman|“V”}} ». Donc selon l'état, le chiffrage est {{Times New Roman|“V”<sup><s>5</s></sup>}}, {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}} ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}. En notation jazz, on ajoute « dim », « <sup>o</sup> » ou bien « <sup>♭5</sup> » au chiffrage, ici : B dim, B<sup>o</sup> ou B<sup>♭5</sup> pour l'état fondamental. Pour les renversements : B dim/D et B dim/F ; ou bien B<sup>o</sup>/D et B<sup>o</sup>/F ; ou bien B<sup>♭5</sup>/D et B<sup>♭5</sup>/F. {{clear}} [[Fichier:Accords basse chiffree basse do fondamental et renverses.svg|vignette|upright=2|Basse chiffrée : accords de quinte, de sixte et de sixte et de quarte ayant pour basse ''do''.]] Et concernant les accords ayant pour basse ''do'' en tonalité de ''do'' majeur : * l'accord ''do''<sup>5</sup> est un accord à l'état fondamental, c'est donc l'accord ''do''-''mi''-''sol'' (sa fondamentale est ''do'') ; * l'accord ''do''<sup>6</sup> est le premier renversement d'un accord, c'est donc l'accord ''do''-''mi''-''la'' (sa fondamentale est ''la'') ; * l'accord ''do''<sup>6</sup><sub>4</sub> est le second renversement d'un accord, c'est donc l'accord ''do''-''fa''-''la'' (sa fondamentale est ''fa''). {{clear}} == Notes étrangères == La musique européenne s'appuie essentiellement sur des accords parfaits, c'est-à-dire fondés sur une tierce majeure ou mineure, et une quinte juste. Il arrive fréquemment qu'un accord ne soit pas un accord parfait. Les notes qui font partie de l'accord parfait sont appelées « notes naturelles » et la note qui n'en fait pas partie est appelée « note étrangère ». Il existe plusieurs types de notes étrangères : * anticipation : la note étrangère est une note naturelle de l'accord suivant ; * appogiature : note d'ornementation qui se résout par mouvement conjoint, c'est-à-dire qu'elle est suivie par une note située juste au-dessus ou en dessous (seconde ascendante ou descendante) qui est, elle, une note naturelle ; * broderie : on part d'une note naturelle, on monte ou on descend d'une seconde, puis on revient sur la note naturelle ; * double broderie : on part d'une note naturelle, on joue la note du dessus puis la note du dessous avant de revenir à la note naturelle ; ou bien on joue la note du dessous puis la note du dessus ; * échappée : note étrangère n'appartenant à aucune des autres catégories ; * note de passage : mouvement conjoint allant d'une note naturelle d'un accord à une note naturelle de l'accord suivant ; * pédale : la note de basse reste la même pendant plusieurs accords successifs ; * retard : la note étrangère est une note naturelle de l'accord précédent. Les notes étrangères ne sont pas chiffrées. [[File:Notes etrangeres accords.svg|center|Différents types de notes étrangères.]] {{note|Les anglophones distinguent deux types de retard : la ''{{lang|en|suspension}}'' est résolue vers le haut (le mouvement est ascendant), le ''{{lang|en|retardation}}'' est résolu vers le bas (le mouvement est descendant).}} == Principaux accords == Les trois principaux accords sont : * l'accord parfait majeur : il est construit sur les degrés {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (médiante) et {{Times New Roman|V}} (dominante) d'une gamme majeure ; il est noté {{Times New Roman|I}}<sup>5</sup>, {{Times New Roman|IV}}<sup>5</sup>, {{Times New Roman|V}}<sup>5</sup> ; * l'accord parfait mineur : il est construit sur les degrés {{Times New Roman|I}} (tonique) et {{Times New Roman|IV}} (sous-tonique) d'une gamme mineure harmonique ; il est également noté {{Times New Roman|I}}<sup>5</sup> et {{Times New Roman|IV}}<sup>5</sup>, les anglo-saxons le notent {{Times New Roman|i}}<sup>5</sup> et {{Times New Roman|iv}}<sup>5</sup> (la minuscule indiquant le caractère mineur) ; * l'accord de septième de dominante : il est construit sur le degré {{Times New Roman|V}} (dominante) d'une gamme majeure ou mineure harmonique ; il est noté {{Times New Roman|V}}<sup>7</sup><sub>+</sub>. On peut trouver ces trois accords sur d'autres degrés, et il existe d'autre types d'accords. Nous verrons cela plus loin. {| class="wikitable" |+ Constitution des principaux accords — Dénomination classique |- ! scope="col" | Accord ! scope="col" | 1<sup>er</sup> intervalle ! scope="col" | 2<sup>e</sup> intervalle ! scope="col" | 3<sup>e</sup> intervalle |- ! scope="row" | Accord parfait majeur | tierce majeure (3M) || quinte juste (5J) || — |- ! scope="row" | Accord parfait mineur | tierce mineure (3m) || quinte juste (5J) || — |- ! scope="row" | Accord de septième de dominante | tierce majeure (3M) || quinte juste (5J) || septième mineure (7m) |} {| class="wikitable" |+ Constitution des principaux accords — Dénomination jazz |- ! scope="col" | Accord ! scope="col" | 1<sup>er</sup> intervalle ! scope="col" | 2<sup>e</sup> intervalle ! scope="col" | 3<sup>e</sup> intervalle |- ! scope="row" | Triade majeure | tierce majeure (3M) || quinte juste (5J) || — |- ! scope="row" | Triade mineure | tierce mineure (3m) || quinte juste (5J) || — |- ! scope="row" | Accord de septième | tierce majeure (3M) || quinte juste (5J) || septième mineure (7m) |} {| border="0" |- | [[Fichier:Accord do majeur arpege puis plaque.midi | Accord parfait de ''do'' majeur (C).]] || [[Fichier:Accord do mineur arpege puis plaque.midi | Accord parfait de ''do'' mineur (Cm).]] || [[Fichier:Accord do septieme arpege puis plaque.midi | Accord de septième de dominante de ''fa'' majeur (C<sup>7</sup>).]] |- | Accord parfait<br /> de ''do'' majeur (C). || Accord parfait<br /> de ''do'' mineur (Cm). || Accord de septième de dominante<br /> de ''fa'' majeur (C<sup>7</sup>). |} '''Rappel :''' * la tierce mineure est composée d'un ton et demi (1 t ½) ; * la tierce majeur est composée de deux tons (2 t) ; * la quinte juste a la même altération que la fondamentale, sauf lorsque la fondamentale est ''si'' (la quinte juste est alors ''fa''♯) ; * la septième mineure est le renversement de la seconde majeure (1 t). [[File:Renversements accords pft fa maj basse chiffree.svg|thumb|Renversements de l'accord parfait de ''fa'' majeur, et la notation de basse chiffrée.]] [[File:Renversements accord sept de dom fa maj basse chiffree.svg|thumb|Renversements de l'accord de septième de dominante de ''fa'' majeur, et la notation de basse chiffrée.]] {| class="wikitable" |+ Notation des principaux accords en musique classique |- ! scope="col" | Accord ! scope="col" | État<br /> fondamental ! scope="col" | Premier<br /> renversement ! scope="col" | Deuxième<br /> renversement ! scope="col" | Troisième<br /> renversement |- ! scope="row" | Accord parfait | {{Times New Roman|I<sup>5</sup>}}<br/> acc. de quinte || {{Times New Roman|I<sup>6</sup>}}<br :> acc. de sixte || {{Times New Roman|I<sup>6</sup><sub>4</sub>}}<br /> acc. de quarte et de sixte || — |- ! scope="row" | Accord de septième<br /> de dominante | {{Times New Roman|V<sup>7</sup><sub>+</sub>}}<br /> acc.de septième de dominante || {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}<br />acc. de sixte et quinte diminuée || {{Times New Roman|V<sup>+6</sup>}}<br />acc. de sixte sensible || {{Times New Roman|V<sup>+4</sup>}}<br />acc. de quarte sensible<br />acc. de triton |} {| class="wikitable" |+ Notation des principaux accords en jazz |- ! scope="col" | Accord ! scope="col" | Chiffrage ! scope="col" | Renversements |- ! scope="row" | Triade majeure | X | rowspan="3" | Les renversements se notent en mettant la basse après une barre de fraction, par exemple pour la triade de ''do'' majeur : * état fondamental : C ; * premier renversement : C/E ; * second renversement : C/G. |- ! scope="row" | Triade mineure | Xm, X– |- ! scope="row" | Septième | X<sup>7</sup> |} {{clear}} Dans le cas d'un accord de septième de dominante, le nom de l'accord change selon que l'on est en musique classique ou en jazz : en musique classique, on donne le nom de la tonalité alors qu'en jazz, on donne le nom de la fondamentale. Ainsi, l'accord appelé « septième de dominante de ''do'' majeur » en musique classique, est appelé « ''sol'' sept » (G<sup>7</sup>) en jazz : la dominante (degré {{Times New Roman|V}}, dominante) de la tonalité de ''do'' majeur est la note ''sol''. Comment appelle-t-on en musique classique l'accord appelé « ''do'' sept » (C<sup>7</sup>) en jazz ? Les tonalités dont le ''do'' est la dominante sont les tonalités de ''fa'' majeur (''si''♭ à la clef) et de ''fa'' mineur harmonique (''si''♭, ''mi''♭, ''la''♭ et ''ré''♭ à la clef et ''mi''♮ accidentel). Il s'agit donc de l'accord de septième de dominante des tonalités de ''fa'' majeur et ''fa'' mineur harmonique. {| class="wikitable" |+ Accords fréquents pour quelques la tonalités majeures ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Accord parfait<br />{{Times New Roman|I<sup>5</sup>}} ! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}} |- |''Do'' majeur || || C<br />''do-mi-sol'' || G7<br />''sol-si-ré-fa'' |- |''Sol'' majeur || ''fa''♯ || G<br />''sol-si-ré'' || D7<br />''ré-fa''♯''-la-do'' |- |''Ré'' majeur || ''fa''♯, ''do''♯ || D<br />''ré-fa''♯''-la'' || A7<br />''la-do''♯''-mi-sol'' |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A<br />''la-do''♯''-mi'' || E7<br />''mi-sol''♯''-si-ré'' |- | ''Fa'' majeur || ''si''♭ || F<br />''fa-la-do'' || C7<br />''do-mi-sol-si''♭ |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭<br />''si''♭''-ré-fa'' || F7<br />''fa-la-do-mi''♭ |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭<br />''mi''♭''-sol-si''♭ || B♭7<br />''si''♭''-ré-fa-la''♭ |} {| class="wikitable" |+ Accords fréquents pour quelques la tonalités mineures harmoniques ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Accord parfait<br />{{Times New Roman|i<sup>5</sup>}} ! scope="col" | Accord de septième<br />de dominante<br />{{Times New Roman|V<sup>7</sup><sub>+</sub>}} |- |''La'' mineur<br />harmonique || || Am, A–<br />''la-do-mi'' || E7<br />''mi-sol''♯''-si-ré'' |- |''Mi'' mineur<br />harmonique || ''fa''♯ || Em, E–<br />''mi-sol-si'' || B7<br />''si-ré''♯''-fa''♯''-la'' |- |''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || Bm, B–<br />''si-ré-fa''♯ || F♯7<br />''fa''♯''la''♯''-do''♯''-mi'' |- |''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || F♯m, F♯–<br />''fa''♯''-la-do''♯ || C♯7<br />''do''♯''-mi''♯''-sol''♯''-si'' |- | ''Ré'' mineur<br />harmonique || ''si''♭ || Dm, D–<br />''ré-fa-la'' || A7<br />''la-do''♯''-mi-sol'' |- | ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || Gm, G–<br />''sol-si''♭''-ré'' || D7<br />''ré-fa''♯''-la-do'' |- | ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || Cm, C–<br />''do-mi''♭''-sol'' || G7<br />''sol-si''♮''-ré-fa'' |} {{clear}} == Accords sur les degrés d'une gamme == === Harmonisation d'une gamme === [[Fichier:Accord trois notes gamme do majeur chiffre.svg|vignette|upright=1.2|Accords de trois note sur la gamme de ''do'' majeur, chiffrés.]] On peut ainsi construire une triade par degré d'une gamme. Pour une gamme majeure, les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|V<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|II<sup>5</sup>}}, {{Times New Roman|III<sup>5</sup>}}, {{Times New Roman|VI<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|ii<sup>5</sup>}}, {{Times New Roman|iii<sup>5</sup>}}, {{Times New Roman|vi<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}. Les accords ont tous une quinte juste à l'exception de l'accord {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} qui a une quinte diminuée, raison pour laquelle le « 5 » est barré. C'est un accord dit « de quinte diminuée ». En jazz, l'accord diminué est noté « dim », « ° », « m<sup>♭5</sup> » ou « <sup>–♭5</sup> ». Nous avons donc trois types d'accords (dans la notation jazz) : X (triade majeure), Xm (triade mineure) et X° (triade diminuée), la lettre X remplaçant le nom de la note fondamentale. {{clear}} [[Fichier:Accord trois notes gamme la mineur chiffre.svg|vignette|upright=1.2|Accords de trois notes sur une gamme de ''la'' mineur harmonique, chiffrés.]] Pour une gamme mineure harmonique, les accords {{Times New Roman|III<sup>+5</sup>}}, {{Times New Roman|V<sup>♯</sup>}} et {{Times New Roman|VI<sup>5</sup>}} ont une tierce majeure. Les accords {{Times New Roman|I<sup>5</sup>}}, {{Times New Roman|II<sup><s>5</s></sup>}}, {{Times New Roman|IV<sup>5</sup>}} et {{Times New Roman|(VII) “V”<sup><s>5</s></sup>}} ont une tierce mineure ; ils sont parfois notés avec des chiffres romains minuscules par les anglo-saxons : {{Times New Roman|i<sup>5</sup>}}, {{Times New Roman|ii<sup><s>5</s></sup>}}, {{Times New Roman|iv<sup>5</sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}}. Les accords {{Times New Roman|ii<sup><s>5</s></sup>}} et {{Times New Roman|(vii) “V”<sup><s>5</s></sup>}} ont une quinte diminuée ; ce sont des accords dits « de quinte diminuée ». L'accord {{Times New Roman|III<sup>+5</sup>}} a une quinte augmentée ; le signe « plus » indique que la note de cinquième, le ''sol'' dièse, est la sensible. En jazz, l'accord est noté « aug » ou « <sup>+</sup> ». Les autres accords ont une quinte juste. Aux trois accords générés par une gamme majeure (X, Xm et X°), nous voyons ici apparaître un quatrième type d'accord : la triade augmentée X<sup>+</sup>. Nous remarquons que des gammes ont des accords communs. Par exemple, l'accord {{Times New Roman|ii<sup>5</sup>}} de ''do'' majeur est identique à l'accord {{Times New Roman|iv<sup>5</sup>}} de ''la'' mineur (il s'agit de l'accord Dm). Quel que soit le mode, les accords construits sur la sensible (accord de quinte diminuée) sont rarement utilisés. S'ils le sont, c'est en tant qu'accord de septième de dominante sans fondamentale (voir ci-après). C'est la raison pour laquelle le chiffrage indique le degré {{Times New Roman|V}} entre guillemets, et non pas le degré {{Times New Roman|VII}} (mais pour des raisons de clarté, nous l'indiquons entre parenthèses au début). En mode mineur, l'accord de quinte augmentée {{Times New Roman|iii<sup>+5</sup>}} est très peu utilisé (voir plus loin ''[[#Progression_d'accords|Progression d'accords]]''). C'est un accord considéré comme dissonant. On voit que : * un accord parfait majeur peut appartenir à cinq gammes différentes ;<br /> par exemple l'accord parfait de ''do'' majeur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> degré de la gamme de ''do'' majeur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' majeur, sur le {{Times New Roman|V}}<sup>e</sup> degré de ''fa'' mineur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''mi'' mineur ; * un accord parfait mineur peut appartenir à cinq gammes différentes ;<br />par exemple l'accord parfait de ''la'' mineur est l'accord construit sur le {{Times New Roman|I}}<sup>er</sup> de la gamme de ''la'' mineur, sur le {{Times New Roman|IV}}<sup>e</sup> degré de ''mi'' mineur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''sol'' majeur, sur le {{Times New Roman|III}}<sup>e</sup> degré de ''fa'' majeur et sur le {{Times New Roman|VI}}<sup>e</sup> degré de ''do'' majeur ; * un accord de quinte diminuée peut appartenir à trois gammes différentes ;<br />par exemple, l'accord de quinte diminuée de ''si'' est l'accord construit sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' majeur, sur le {{Times New Roman|II}}<sup>e</sup> degré de ''la'' mineur et sur le {{Times New Roman|VII}}<sup>e</sup> degré de ''do'' mineur ; * un accord de quinte augmentée (à l'état fondamental) ne peut appartenir qu'à une seule gamme ;<br /> par exemple, l'accord de quinte augmentée de ''do'' est l'accord construit sur le {{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur. {| class="wikitable" |+ Notation jazz des triades |- | rowspan="2" colspan="2" | ! scope="col" colspan="2" | Tierce |- ! scope="col" | 3m ! scope="col" | 3M |- ! rowspan="3" | Quinte ! scope="row" | 5d | Xᵒ, X<sub>m</sub><sup>(♭5)</sup> || |- ! scope="row" | 5J | Xm, X– || X |- ! scope="row" | 5A | || X+, X<sup>(♯5)</sup> |} === Harmonisation par des accords de septième === [[Fichier:Harmonisation gamme do majeur par septiemes chiffre.svg|vignette|upright=2|Harmonisation de la gamme de do majeur par des accords de septième.]] Les accords de septième contiennent une dissonance et créent ainsi une tension. Ils sont très utilisés en jazz. Nous avons représenté ci-contre l'harmonisation de la gamme de ''do'' majeur. La constitution des accords est la suivantes : * tierce majeure (3M) ** quinte juste (5J) *** septième mineure (7m) : sur le degré V, c'est l'accord de septième de dominante V<sup>7</sup><sub>+</sub>, noté X<sup>7</sup> (X pour G), *** septième majeure (7M) : sur les degrés I et IV, appelés « accords de septième majeure » et notés aussi X<sup>maj7</sup> ou X<sup>Δ</sup> (X pour C ou F) ; * tierce mineure (3m) ** quinte juste (5J) *** septième mineure : sur les degrés ii, iii et vi, appelés « accords mineur septième » et notés Xm<sup>7</sup> ou X–<sup>7</sup> (X pour D, E ou A), ** quinte diminuée (5d) *** septième mineure (7m) : sur le degré vii, appelé « accord demi-diminué » (puisque seule la quinte est diminuée) et noté X<sup>∅</sup> ou Xm<sup>7(♭5)</sup> ou X–<sup>7(♭5)</sup> (X pour B) ;<br /> en musique classique, on considère que c'est un accord de neuvième de dominante sans fondamentale. Nous avons donc quatre types d'accords : X<sup>7</sup>, X<sup>maj7</sup>, Xm<sup>7</sup> et X<sup>∅</sup> En jazz, on ajoute souvent la quarte à l'accord de sous-dominante IV (sur le ''fa'' dans une gamme de ''do'' majeur) ; il s'agit ici d'une quarte augmentée (''fa''-''si'') et l'accord est surnommé « accord lydien » mais cette dénomination est erronée (il s'agit d'une mauvaise interprétation de textes antiques). C'est un accord de onzième sans neuvième (la onzième étant l'octave de la quarte), il est noté X<sup>maj7(♯11)</sup> ou X<sup>Δ(♯11)</sup> (ici, F<sup>maj7(♯11)</sup>, ''fa''-''la''-''do''-''mi''-''si'' ou ''fa''-''la''-''si''-''do''-''mi''). {| class="wikitable" |+ Chiffrage jazz des accords de septième |- ! scope="col" rowspan="2" | Tierce ! scope="col" rowspan="2" | Quinte ! scope="col" colspan="2" | Septième |- ! scope="col" | 7m ! scope="col" | 7M |- | rowspan="2" | 3m | 5d || X<sup>∅</sup>, X<sub>m</sub><sup>7(♭5)</sup>, X–<sup>7(♭5)</sup> || |- | rowspan="2" | 5J | X<sub>m</sub><sup>7</sup>, X–<sup>7</sup> || |- | rowspan="2" | 3M | X<sup>7</sup> || X<sup>maj7</sup>, X<sup>Δ</sup> |- | 5A || || X<sub>+</sub><sup>maj7</sup>, X<sub>+</sub><sup>Δ</sup> |} === Modulation et emprunt === Un morceau peut comporter des changements de tonalité; appelés « modulation ». Il y a parfois un court passage dans une tonalité différente, typiquement sur une ou deux mesures, avant de retourner dans la tonalité d'origine : on parle d'emprunt. Lorsqu'il y a une modulation ou un emprunt, les degrés changent. Un même accord peut donc avoir une fonction dans une partie du morceau et une autre fonction ailleurs. L'utilisation d'accord différents, et en particulier d'accord utilisant des altérations accidentelles, indique clairement une modulation. Nous avons vu précédemment que les modulations courantes sont : * les modulations dans les tons voisins ; * les modulations homonymes ; * les marches harmoniques. Une modulation entre une tonalité majeure et mineure change la couleur du passage, * la modulation la plus « douce » est entre les tonalités relatives (par exemple''do'' majeur et ''la'' mineur) car ces tonalités utilisent quasiment les mêmes notes ; * la modulation la plus « voyante » est la modulation homonyme (par exemple entre ''do'' majeur et ''do'' mineur). Une modulation commence souvent sur l'accord de dominante de la nouvelle tonalité. Pour analyser un œuvre, ou pour improviser sur une partie, il est important de reconnaître les modulations. La description de la successind es tonalités s'appelle le « parcours tonal ». === Exercices élémentaires === L'apprentissage des accords passe par quelques exercices élémentaires. '''1. Lire un accord''' Il s'agit de lecture de notes : des notes composant les accords sont écrites « empilées » sur une portée, il faut les lire en énonçant les notes de bas en haut. '''2. Reconnaître la « couleur » d'un accord''' On écoute une triade et il faut dire si c'est une triade majeure ou mineure. Puis, on complexifie l'exercice en ajoutant la septième. '''3. Chiffrage un accord''' Trouver le nom d'un accord à partir des notes qui le composent. '''4. Réalisation d'un accord''' Trouver les notes qui composent un accord à partir de son nom. '''5. Dictée d'accords''' On écoute une succession d'accords et il faut soit écrire les notes sur une portée, soit écrire les noms de accords. [[File:Exercice constitution accord basse chiffree.svg|thumb|Exercice : constitution d'accord à partir de la basse chiffrée.]] '''Exercices de basse chiffrée''' ''Réalisation d'un accord'' Sur la figure suivante, écrire les notes des accords correspondant à la basse chiffrée. Déterminer le degré de la fondamentale pour chaque accord en considérant que nous sommes dans la tonalité de ''sol'' majeur. {{boîte déroulante/début|titre=Solution}} [[File:Exercice constitution accord basse chiffree solution.svg|vignette|Solution.]] # La note de basse est un ''do''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''mi'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''sol''.<br />Le chiffrage « <sup>5</sup> » indique que c'est un accord dans son état fondamental (l'écart entre deux notes consécutives ne dépasse pas la tierce), la fondamentale est donc la basse, ''do'', qui est le degré IV de la tonalité. # La note de basse est un ''si''. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''ré'', puis nous appliquons le chiffrage 6 et ajoutons la sixte, ''sol''.<br />Le chiffrage « <sup>6</sup> » indique que c'est un accord dans son premier renversement. En le remettant dans son état fondamental, nous obtenons ''sol-si-ré'', la fondamentale est donc la tonique, le degré I. # La note de basse est un ''la''. Nous ajoutons la tierce (chiffre 3), ''do'', et la sixte (6), ''fa''♯. Nous vérifions que le ''fa''♯ est la sensible (signe +)<br />Nous voyons un « blanc » entre les notes ''do'' et ''fa''♯. En descendant le ''fa''♯ à l'octave inférieure, nous obtenons un empilement de tierces ''fa''♯-''la-do'', le fondamentale est donc ''fa''♯, le degré VII. Nous pouvons le voir comme le deuxième renversement de l'accord de septième de dominante, sans fondamentale. # La note de basse est un ''fa''♯. Le chiffrage ne contient pas de 2 ni de 4. Nous ajoutons donc la tierce, ''la'', puis nous appliquons le chiffrage 5 et ajoutons la quinte, ''do'' ; nous vérifions qu'il s'agit bien d'une quinte diminuée (le 5 est barré). Nous appliquons le chiffre 6 et ajoutons la sixte, ''ré''.<br />Nous voyons que les notes ''do'' et ''ré'' sont conjointes (intervalle de seconde). En descendant le ''ré'' à l'octave inférieure, nous obtenons un empilement de tierces ''ré-fa''♯-''la-do'', le fondamentale est donc ''ré'', le degré V. Nous constatons que l'accord chiffré est le premier renversement de l'accord de septième de dominante. {{boîte déroulante/fin}} {{clear}} ''Chiffrage d'accords'' [[Fichier:Exercice chiffrage accord basse chiffree.svg|vignette|Accords à chiffrer.]] Chiffrer les accords ci-contre. {{boîte déroulante/début|titre=Solution}} [[File:Exercice chiffrage accord basse chiffree solution.svg|vignette|Solution.]] # On relève les intervalles en partant de la basse : tierce majeure (3M) et quinte juste (5J). Le chiffrage complet est donc ''fa''<sup>5</sup><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''fa''<sup>5</sup>.<br /> On peut aussi reconnaître que c'est l'accord parfait sur la tonique de la tonalité de ''fa'' majeur dans son état fondamental, le chiffrage d'un accord parfait étant <sup>5</sup>. # On relève les intervalles en partant de la basse : quarte juste (4J), sixte majeure (6M). Le chiffrage complet est donc ''fa''<sup>6</sup><sub>4</sub>.<br /> On peut aussi reconnaître que c'est le second renversement de l'accord ''mi-sol-si'', sur la tonique de la tonalité de ''mi'' mineur, le chiffrage du second renversement d'un accord parfait étant <sup>6</sup><sub>4</sub>. # Les intervalles en partant de la basse sont : tierce mineure (3m), quinte diminuée (5d), sixte mineure (6m). Le chiffrage complet est donc ''mi''<sup>6</sup><small><s>5</s></small><sub>3</sub>. On simplifie en enlevant le 3, le chiffrage est donc ''mi''<sup>6</sup><sub><s>5</s></sub>.<br /> On reconnaît le premier renversement de l'accord ''do-mi-sol-si''♭, accord de septième de dominante de la tonalité de ''fa'' majeur. # Les intervalles en partant de la basse sont : tierce mineure (3m), quinte juste (5J), septième mineure (7m). Le chiffrage complet est donc ''ré''<sup>7</sup><small>5</small><sub>3</sub> ; c'est typique d'un accord de septième de dominante, son chiffrage est donc ''ré''<sup>7</sup><sub>+</sub>.<br /> On reconnaît l'accord de septième de dominante de la tonalité de ''sol'' mineur dans son état fondamental. {{boîte déroulante/fin}} {{clear}} [[File:Exercice constitution accord notation jazz.svg|thumb|Exercice : constitution d'un accord d'après son chiffrage en notation jazz.]] '''Exercices de notation jazz''' ''Réalisation d'un accord'' Sur la figure suivante, écrire les notes des accords correspondant aux chiffrages. {{boîte déroulante/début|titre=Solution}} [[File:Exercice constitution accord notation jazz solution.svg|thumb|Solution.]] # Il s'agit de la triade majeure de ''do'' dans son état fondamental. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''do-mi-sol''. # Il s'agit de la triade majeure de ''sol''. Les intervalles en partant de la fondamentale sont la tierce majeure (3M) et la quinte juste (5J). Les notes sont donc ''sol-si-ré''. On renverse l'accord afin que la basse soit le ''si'', l'accord est donc ''si-ré-sol''. # Il s'agit de l'accord demi-diminué de ''fa''♯. Les intervalles sont la tierce mineure (3m), la quinte diminuée (5d) et la septième mineure (7m). Les notes sont donc ''fa''♯-''la-do-mi''. Nous renversons l'accord afin que la basse soit le ''la'', l'accord est donc ''a-do-mi-fa''♯. # Il s'agit de l'accord de septième de ''ré''. Les intervalles sont donc la tierce majeure (3M), la quinte juste (5J) et la septième mineure (7m). Les notes sont ''ré-fa''♯''-la-do''. Nous renversons l'accord afin que la basse soit le ''fa''♯, l'accord est donc ''fa''♯''-la-do-ré''. {{boîte déroulante/fin}} {{clear}} ''Chiffrage d'accords'' [[File:Exercice chiffrage accord notation jazz.svg|thumb|Accords à chiffrer.]] Chiffrer les accords ci-contre. {{boîte déroulante/début|titre=Solution}} [[File:Exercice chiffrage accord notation jazz solution.svg|thumb|Solution.]] # Les notes sont toutes sur des interlignes consécutifs, c'est donc un empilement de tierces ; l'accord est dans son état fondamental. Les intervalles sont une tierce majeure (''fa-la'' : 3M) et une quinte juste (''fa-do'' : 5J), c'est donc la triade majeure de ''fa''. Le chiffrage est F. # Il y a un blanc dans l'empilement des notes, c'est donc un accord renversé. En permutant les notes pour n'avoir que des tierces, on trouve l'accord ''mi-sol-si''. Les intervalles sont une tierce mineure (''mi-sol'' : 3m) et une quinte juste (''mi-si'' : 5J), c'est donc la triade mineure de ''mi'' avec un ''si'' à la basse. Le chiffrage est Em/B ou E–/B. # Il y deux notes conjointes, c'est donc un renversement. L'état fondamental de cet accord est ''do-mi-sol-si''♭. Les intervalles sont une tierce majeure (''do-mi'' : 3M), une quinte juste (''do-sol'' : 5J) et une septième mineure (''do-si''♭ : 7m). C'est donc l'accord de ''do'' septième avec un ''mi'' à la basse, chiffré C<sup>7</sup>/E. # Les notes sont toutes sur des interlignes consécutifs, l'accord est dans son état fondamental. Les intervalles sont la tierce mineure (''ré-fa'' : 3m), une quinte juste (''ré-la'' : 5J) et une septième mineure (''ré-do'' : 7m). C'est donc l'accord de ''ré'' mineur septième, chiffré Dm<sup>7</sup> ou D–<sup>7</sup>. {{boîte déroulante/fin}} {{clear}} == Harmonie fonctionnelle == Le choix des accords et de leur succession — la progression des accords — est un élément important d'un morceau, de sa composition. Le compositeur ou la compositrice a bien sûr une liberté totale, mais pour faire des choix, il faut comprendre les conséquences de ces choix, et donc ici, les effets produits par les accords et leur progression. Une des manières d'aborder le sujet est l'harmonie fonctionnelle. === Les trois fonctions des accords === En harmonie tonale, on considère que les accords ont une fonction. Il existe trois fonctions : * la fonction de tonique, {{Times New Roman|I}} ; * la fonction de sous-dominante, {{Times New Roman|IV}} ; * la fonction de dominante, {{Times New Roman|V}}. L'accord de tonique, {{Times New Roman|I}}, est l'accord « stable » de la tonalité par excellence. Il conclut en général les morceaux, et ouvre souvent les morceaux ; il revient fréquemment au cours du morceau. L'accord de dominante, {{Times New Roman|V}}, est un accord qui introduit une instabilité, une tension. En particulier, il contient la sensible (degré {{Times New Roman|VI}}), qui est une note « aspirée » vers la tonique. Cette tension, qui peut être renforcée par l'utilisation d'un accord de septième, est fréquemment résolue par un passage vers l'accord de tonique. Nous avons donc deux mouvements typiques : {{Times New Roman|I}} → {{Times New Roman|V}} (création d'une tension, d'une attente) et {{Times New Roman|V}} → {{Times New Roman|I}} (résolution d'une tension). Les accords de tonique et de dominante ont le cinquième degré en commun, cette note sert donc de pivot entre les deux accords. L'accord de sous-dominante, {{Times New Roman|IV}}, est un accord qui introduit lui aussi une tension, mais moins grande : il ne contient pas la sensible. Notons que s'il est une quarte au-dessus de la tonique, il est aussi une quinte en dessous d'elle ; il est symétrique de l'accord de dominante. Il a donc un rôle similaire à l'accord de dominante, mais atténué. L'accord de sous-dominante aspire soit vers l'accord de dominante, très proche, et l'on a alors une augmentation de la tension ; soit vers l'accord de tonique, un retour vers la stabilité (il a alors un rôle semblable à la dominante). Du fait de ces deux bifurcations possibles — augmentation de la tension ({{Times New Roman|IV}} → {{Times New Roman|V}}) ou retour à la stabilité ({{Times New Roman|IV}} → {{Times New Roman|I}}) —, l'utilisation de l'accord de sous-dominante introduit un certain flottement : si l'on peut facilement prédire l'accord qui suit un accord de dominante, on ne peut pas prédire ce qui suit un accord de sous-dominante. Notons que la composition ne consiste pas à suivre ces règles de manière stricte, ce qui conduirait à des morceaux stéréotypés et plats. Le plaisir d'écoute joue sur une alternance entre satisfaction d'une attente (respect des règles) et surprise (rompre les règles). === Accords remplissant ces fonctions === Les accords sur les autres degrés peuvent se ramener à une de ces trois fonctions : * {{Times New Roman|II}} : fonction de sous-dominante {{Times New Roman|IV}} ; * {{Times New Roman|III}} (très peu utilisé en mode mineur en raison de sa dissonance) et {{Times New Roman|VI}} : fonction de tonique {{Times New Roman|I}} ; * {{Times New Roman|VII}} : fonction de dominante {{Times New Roman|V}}. En effet, les accords étant des empilements de tierces, des accords situés à une tierce l'un de l'autre — {{Times New Roman|I}} ↔ {{Times New Roman|III}}, {{Times New Roman|II}} ↔ {{Times New Roman|IV}}, {{Times New Roman|V}} ↔ {{Times New Roman|VII}}, {{Times New Roman|VI}} ↔ {{Times New Roman|VIII}} ( = {{Times New Roman|I}}) — ont deux notes en commun. On retrouve le fait que l'accord sur le degré {{Times New Roman|VII}} est considéré comme un accord de dominante sans tonique. En mode mineur, l'accord sur le degré {{Times New Roman|III}} est évité, il n'a donc pas de fonction. {|class="wikitable" |+ Fonction des accords |- ! scope="col" | Fondamentale ! scope="col" | Fonction |- | {{Times New Roman|I}} || tonique |- | {{Times New Roman|II}} || sous-dominante faible |- | {{Times New Roman|III}} || tonique faible |- | {{Times New Roman|IV}} || sous-dominante |- | {{Times New Roman|V}} || dominante |- | {{Times New Roman|VI}} || tonique faible |- | {{Times New Roman|VII}} || dominante faible |} Par exemple en ''do'' majeur : * fonction de tonique : '''''do''<sup>5</sup> (C)''', ''mi''<sup>5</sup> (E–), ''la''<sup>5</sup> (A–) ; * fonction de sous-dominante : '''''fa''<sup>5</sup> (F)''', ''ré''<sup>5</sup> (D–) ; * fonction de dominante : '''''sol''<sup>5</sup> (G)''' ou ''sol''<sup>7</sup><sub>+</sub> (G<sup>7</sup>), ''si''<sup> <s>5</s></sup> (B<sup>o</sup>). En ''la'' mineur harmonique : * fonction de tonique : '''''la''<sup>5</sup> (A–)''', ''fa''<sup>5</sup> (F) [, rarement : ''do''<sup>+5</sup> (C<sup>+</sup>)] ; * fonction de sous-dominante : '''''ré''<sup>5</sup> (D–)''', ''si''<sup> <s>5</s></sup> (B<sup>o</sup>) ; * fonction de dominante : '''''mi''<sup>5</sup> (E)''' ou ''mi''<sup>7</sup><sub>+</sub> (E<sup>7</sup>), ''sol''♯<sup> <s>5</s></sup> (G♯<sup>o</sup>). Le fait d'utiliser des accords différents pour remplir une fonction permet d'enrichir l'harmonie, et de jouer sur l'équilibre entre satisfaction d'une attente (on respecte les règles sur les fonctions) et surprise (mais on n'utilise pas l'accord attendu). === Les dominantes secondaires === On utilise aussi des accords de septième dominante se fondant sur un autre degré que la dominante de la gamme ; on parle de « dominante secondaire ». Typiquement, avant un accord de septième de dominante, on utilise parfois un accord de dominante de dominante, dont le degré est alors noté « {{Times New Roman|V}} de {{Times New Roman|V}} » ou « {{Times New Roman|V}}/{{Times New Roman|V}} » ; la fondamentale est de l'accord est alors situé cinq degrés au-dessus de la dominante ({{Times New Roman|V}}), c'est donc le degré {{Times New Roman|IX}}, c'est-à-dire le degré {{Times New Roman|II}} de la tonalité en cours). Ou encore, on utilise un accord de dominante du degré {{Times New Roman|IV}} (« {{Times New Roman|V}} de {{Times New Roman|IV}} », la fondamentale est alors le degré {{Times New Roman|I}}) avant un accord sur le degré {{Times New Roman|IV}} lui-même. Par exemple, en tonalité de ''do'' majeur, on peut trouver un accord ''ré - fa''♯'' - la - do'' (chiffré {{Times New Roman|V}} de {{Times New Roman|V}}<sup>7</sup><sub>+</sub>), avant un accord ''sol - si - ré - fa'' ({{Times New Roman|V}}<sup>7</sup><sub>+</sub>). L'accord ''ré - fa''♯'' - la - do'' est l'accord de septième de dominante des tonalités de ''sol''. Dans la même tonalité, on pourra utiliser un accord ''do - mi - sol - si''♭ ({{Times New Roman|V}} de {{Times New Roman|IV}}<sup>7</sup><sub>+</sub>) avant un accord ''fa - la - do'' ({{Times New Roman|IV}}<sup>5</sup>). Le recours à une dominante secondaire peut atténuer une transition, par exemple avec un enchaînement ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> → ''fa''<sup>5</sup> (C → C<sup>7</sup> → F) qui correspond à un enchaînement {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}} : le passage ''do''<sup>5</sup> → ''do''<sup>7</sup><sub>+</sub> (C → C<sup>7</sup>) se fait en ajoutant une note (le ''si''♭) et rend naturel le passage ''do'' → ''fa''. Sur les sept degré de la gamme, on ne considère en général que cinq dominantes secondaires : en effet, la dominante du degré {{Times New Roman|I}} est la dominante « naturelle, primaire » de la tonalité (et n'est donc pas secondaire) ; et utiliser la dominante de {{Times New Roman|VII}} consisterait à considérer l'accord de {{Times New Roman|VII}} comme un accord propre, on évite donc les « {{Times New Roman|V}} de “{{Times New Roman|V}}” » (mais les « “{{Times New Roman|V}}” de {{Times New Roman|V}} » sont tout à fait « acceptables »). === Enchaînements classiques === Nous avons donc vu que l'on trouve fréquemment les enchaînements suivants : * pour créer une instabilité : ** {{Times New Roman|I}} → {{Times New Roman|V}}, ** {{Times New Roman|I}} → {{Times New Roman|IV}} (instabilité moins forte mais incertitude sur le sens d'évolution) ; * pour maintenir l'instabilité : ** {{Times New Roman|IV}} → {{Times New Roman|V}} ; * pour résoudre l'instabilité : ** {{Times New Roman|IV}} → {{Times New Roman|I}}, ** {{Times New Roman|V}} → {{Times New Roman|I}}, cas particuliers (voir plus bas) : *** {{Times New Roman|V}}<sup>+4</sup> → {{Times New Roman|I}}<sup>6</sup>, *** {{Times New Roman|I}}<sup>6</sup><sub>4</sub> → {{Times New Roman|V}}<sup>7</sup><sub>+</sub> → {{Times New Roman|I}}<sup>5</sup>. Les degrés indiqués ci-dessus sont les fonctions ; on peut donc utiliser les substitutions suivantes : * {{Times New Roman|I}} par {{Times New Roman|VI}} et, en tonalité majeure, {{Times New Roman|III}} ; * {{Times New Roman|IV}} par {{Times New Roman|II}} ; * {{Times New Roman|V}} par {{Times New Roman|VII}}. Pour enrichir l'harmonie, on peut utiliser les dominantes secondaires, en particulier : * {{Times New Roman|V}} de {{Times New Roman|V}} ({{Times New Roman|II}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|V}}, * {{Times New Roman|V}} de {{Times New Roman|IV}} ({{Times New Roman|I}}<sup>7</sup><sub>+</sub>) → {{Times New Roman|IV}}. On peut enchaîner les enchaînements, par exemple {{Times New Roman|I}} → {{Times New Roman|IV}} → {{Times New Roman|V}}, ou encore {{Times New Roman|I}} → {{Times New Roman|V}} de {{Times New Roman|IV}} → {{Times New Roman|IV}}… En jazz, on utilise très fréquemment l'enchaînement {{Times New Roman|II}} → {{Times New Roman|V}} → {{Times New Roman|I}} (deux-cinq-un). On peut bien sûr avoir d'autres enchaînements, mais ces règles permettent d'analyser un grand nombre de morceaux, et donnent des clefs utiles pour la composition. Nous voyons ci-après un certain nombre d'enchaînements courants dans différents styles == Exercice == Un hautboïste travaille la sonate en ''do'' mineur S. 277 de Heinichen. Sur le deuxième mouvement ''Allegro'', il a du mal à travailler un passage en raison des altérations accidentelles. Sur la suggestion de sa professeure, il décide d'analyser la progression d'accords sous-jacente afin que les altérations deviennent logiques. Il s'agit d'un duo hautbois et basson pour lequel les accords ne sont pas chiffrés, le basson étant ici un instrument soliste et non pas un élément de la basse continue. Sur l'extrait suivant, déterminez les basses et la qualité (chiffrage) des accords sous-jacents. Commentez. [[Fichier:Sonate hautbois basson heinichen 2e mvt mes49.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen.]] {{note|L'œuvre est en ''do'' mineur et devrait donc avoir trois bémols à la clef, or ici il n'y en a que deux. En effet, le ''la'' pouvant être bécarre en mode mineur mélodique ascendant, le compositeur a préféré le noter explicitement en altération accidentelle lorsque l'on est en mode mélodique naturel, harmonique ou mélodique descendant. C'est un procédé assez courant à l'époque baroque.}} {{boîte déroulante/début|titre=Solution}} Une des difficultés ici est que les arpèges joués par les instruments sont agrémentés de notes de passage. Les notes de la basse (du basson) sont différentes entre le premier et le deuxième temps de chaque mesure et ne peuvent pas appartenir au même accord. On a donc un accord par temps. Sur le premier temps de chaque mesure, le basson joue une octave. La note concernée est donc la basse de chaque accord. Pour savoir s'il s'agit d'un accord à l'état fondamental ou d'un renversement, on regarde ce que joue le hautbois : dans un mouvement conjoint (succession d'intervalles de secondes), il est difficile de distinguer les notes de l'arpège des notes de passage, mais : les notes des grands intervalles font partie de l'accord. Ainsi, sur le premier temps de la première mesure (la basse est un ''mi''♭), on a une sixte descendante ''sol''-''si''♭ et, à la fin du temps, une tierce descendante ''sol''-''mi''♭. L'accord est donc ''mi''♭-''sol''-''si''♭, c'est un accord de quinte (accord parfait à l'état fondamental). À la fin du premier temps, le basson joue un ''do'', c'est donc une note étrangère. Sur le second temps de la première mesure, le basson joue une tierce ascendante ''fa''-''la''♭, la première note est la basse de l'accord et la seconde une des notes de l'accord. Le hautbois commence par une sixte descendante ''la''♭-''do'', l'accord est donc ''fa''-''la''♭-''do'', un accord de quinte (accord parfait à l'état fondamental). Le ''do'' du basson la fin du premier temps est donc une anticipation. Les autres notes étrangères de la première mesure sont des notes de passage. Mais il faut faire attention : en suivant ce principe, sur les premiers temps des deuxième et troisième mesure, nous aurions des accords de septième d'espèce (puisque la septième est majeure). Or, on ne trouve pas, ou alors exceptionnellement, d'accord de septième d'espèce dans le baroque, mais quasi exclusivement des accords de septième de dominante. Donc au début de la deuxième mesure, le ''la''♮ est une appoggiature du ''si''♭, l'accord est donc ''si''♭-''ré''-''fa'', un asscord de quinte. De même, au début de la troisième mesure, le ''sol'' est une appoggiature du ''la''♭. Il faut donc se méfier d'une analyse purement « mathématique ». Il faut s'attacher à ressentir la musique, et à connaître les styles, pour faire une analyse pertinente. Ci-dessous, nous avons grisé les notes étrangères. [[Fichier:Sonate hautbois basson heinichen 2e mvt mes49 analyse.svg|center|Extrait du deuxième mouvement Allegro de la sonate en trio en do mineur S. 277 de Johann David Heinichen. Analyse de la progression harmonique.]] Le chiffrage jazz équivalent est : : | E♭ F– | B♭<sup>Δ</sup> E♭ | A♭<sup>Δ</sup> D– | G … Nous remarquons une progression assez régulière : : ''mi''♭ ↗[2<sup>de</sup>] ''fa'' | ↘[5<sup>te</sup>] ''si''♭ ↗[4<sup>te</sup>] ''mi''♭ | ↘[5<sup>te</sup>] ''la''♭ ↗[4<sup>te</sup>] ''ré'' | ↘[5<sup>te</sup>] ''sol'' Le ''mi''♭ est le degré {{Times New Roman|III}} de la tonalité principale (''do'' mineur), c'est donc une tonique faible ; il « joue le même rôle » qu'un ''do''. S'il y avait eu un accord de ''do'' au début de l'extrait, on aurait eu une progression parfaitement régulière ↗[4<sup>te</sup>] ↘[5<sup>te</sup>]. Nous avons les modulations suivantes : * mesure 49 : ''do'' mineur naturel (le ''si''♭ n'est pas une sensible) avec un accord sur “{{Times New Roman|I}}” (tonique faible, {{Times New Roman|III}}, pour la première analyse, ou bien tonique forte, {{Times New Roman|I}}, pour la seconde) suivi d'un accord sur {{Times New Roman|IV}} ; * mesure 50 : ''si''♭ majeur avec un accord sur {{Times New Roman|I}} suivi d'un accord sur {{Times New Roman|IV}} ; * mesure 51 : ''la''♭ majeur avec un accord sur {{Times New Roman|I}}, et emprunt à ''do'' majeur avec un accord sur {{Times New Roman|II}} ({{Times New Roman|IV}} faible). On a donc une marche harmonique {{Times New Roman|I}} → {{Times New Roman|IV}} qui descend d'une seconde majeure (un ton) à chaque mesure (''do'' → ''si''♭ → ''la''♭), avec une exception sur la dernière mesure (modulation en cours de mesure et descente d'une seconde mineure au lieu de majeure). Ce passage est donc construit sur une régularité, une règle qui crée un effet d'attente — enchaînement {{Times New Roman|I}}<sup>5</sup> → {{Times New Roman|IV}}<sup>5</sup> avec une marche harmonique d'une seconde majeure descendante —, et des « surprises », des exceptions au début — ce n'est pas un accord {{Times New Roman|I}}<sup>5</sup> mais un accord {{Times New Roman|III}}<sup>5</sup> — et à la fin — modulation en milieu de mesure et dernière descente d'une seconde mineure (½t ''la''♭ → ''sol''). L'extrait ne permet pas de le deviner, mais la mesure 52 est un retour en ''do'' mineur, avec donc une modulation sur la dominante (accord de ''sol''<sup>7</sup><sub>+</sub>, G<sup>7</sup>). {{boîte déroulante/fin}} == Progression d'accords == Comme pour la mélodie, la succession des accords dans un morceau, la progression d'accords, suit des règles. Et comme pour la mélodie, les règles diffèrent d'un style musical à l'autre et la créativité consiste à parfois ne pas suivre ces règles. Et comme pour la mélodie, on part d'un ensemble de notes organisé, d'une gamme caractéristique d'une tonalité, d'un mode. Les accords les plus utilisés pour une tonalité donnée sont les accords dont la fondamentale sont les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la tonalité, en particulier la triade {{Times New Roman|I}}, appelée « accord parfait » ou « accord de tonique », et l'accord de septième {{Times New Roman|V}}, appelé « septième de dominante ». Le fait d'avoir une progression d'accords qui se répète permet de structurer un morceau. Pour les morceaux courts, il participe au plaisir de l'écoute et facilite la mémorisation (par exemple le découpage couplet-refrain d'une chanson). Sur les morceaux longs, une trop grande régularité peut introduire de la lassitude, les longs morceaux sont souvent découpés en parties présentant chacune une progression régulière. Le fait d'avoir une progression régulière permet la pratique de l'improvisation : cadence en musique classique, solo en jazz et blues. ; Note : Le terme « cadence » désigne plusieurs choses différentes, et notamment en harmonie : :* une partie improvisée dans un opéra ou un concerto, sens utilisé ci-dessus ; :* une progression d'accords pour ponctuer un morceau et en particulier pour le conclure, sens utilisé dans la section suivante. === Accords peu utilisés === En mode mineur, l'accord de quinte augmentée {{Times New Roman|III<sup>+5</sup>}} est très peu utilisé. C'est un accord dissonant ; il intervient en général comme appogiature de l'accord de tonique (par exemple en ''la'' mineur : {{Times New Roman|III<sup>+5</sup>}} ''do'' - ''mi'' - ''sol''♯ → {{Times New Roman|I<sup>6</sup>}} ''do'' - ''mi'' - ''la''), ou de l'accord de dominante ({{Times New Roman|III<sup>6</sup><sub>+3</sub>}} ''mi'' - ''sol''♯ - ''do'' → {{Times New Roman|V<sup>5</sup>}} ''mi'' - ''sol''♯ - ''si''). Il peut être aussi utilisé comme préparation à l'accord de sous-dominante (enchaînement {{Times New Roman|III}} → {{Times New Roman|IV}}). Par ailleurs, il a une constitution symétrique — c'est l'empilement de deux tierces majeures — et ses renversements ont les mêmes intervalles à l'enharmonie près (quinte augmentée/sixte mineure, tierce majeure/quarte diminuée). De ce fait, un même accord est commun, par renversement et à l'enharmonie près, à trois tonalités : le premier renversement de l'accord ''do'' - ''mi'' - ''sol''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''la'' mineur) est enharmonique à ''mi'' - ''sol''♯ - ''si''♯ ({{Times New Roman|III}}<sup>e</sup> degré de ''do''♯ mineur) ; le second renversement est enharmonique à ''la''♭ - ''do'' - ''mi'' ({{Times New Roman|III}}<sup>e</sup> degré de ''fa'' mineur). === Accords très utilisés === Les trois accords les plus utilisés sont les accords de tonique (degré {{Times New Roman|I}}), de sous-dominante ({{Times New Roman|IV}}) et de dominante ({{Times New Roman|V}}). Ils interviennent en particulier en fin de phrase, dans les cadences. L'accord de dominante sert souvent à introduire une modulation : la modulation commence sur l'accord de dominante de la nouvelle tonalité. On note que l'accord de sous-dominante est situé une quinte juste en dessous de la tonique, les accords de dominante et de sous-dominante sont donc symétriques. En jazz, on utilise également très fréquemment l'accord de la sus-tonique (degré {{Times New Roman|II}}), souvent dans des progressions {{Times New Roman|II}} - {{Times New Roman|V}} (- {{Times New Roman|I}}). Rappelons que l'accord de sus-tonique a la fonction de sous-dominante. === Cadences et ''turnaround'' === Le terme « cadence » provient de l'italien ''cadenza'' et désigne la « chute », la fin d'un morceau ou d'une phrase musicale. On distingue deux types de cadences : * les cadences conclusive, qui créent une sensation de complétude ; * les cadences suspensives, qui crèent une sensation d'attente. ==== Cadence parfaite ==== [[Fichier:Au clair de le lune cadence parfaite.midi|thumb|''Au clair de la lune'', harmonisé avec une cadence parfaite (italienne).]] [[Fichier:Au clair de le lune mineur cadence parfaite.midi|thumb|''Idem'' mais en mode mineur harmonique.]] La cadence parfaite est l'enchaînement de l'accord de dominante suivi de l'accord parfait : {{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}}, les deux accord étant à l'état fondamental. Elle donne une impression de stabilité et est donc très souvent utilisée pour conclure un morceau. C'est une cadence conclusive. On peut aussi utiliser l'accord de septième de dominante, la dissonance introduisant une tension résolue par l'accord parfait : {{Times New Roman|V<sup>7</sup><sub>+</sub> - I<sup>5</sup>}}. Elle est souvent précédée de l'accord construit sur le IV<sup>e</sup> degré, appelé « accord de préparation », pour former la cadence italienne : {{Times New Roman|IV<sup>5</sup> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}}. Elle est également souvent précédée du second renversement de l'accord de tonique, qui est alors appelé « appoggiature de la cadence » : {{Times New Roman|I<sup>6</sup><sub>4</sub> - V<sup>5</sup>}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}) {{Times New Roman|- I<sup>5</sup>}} (on remarque que les accords {{Times New Roman|I}}<sup>6</sup><sub>4</sub> et {{Times New Roman|V}}<sup>5</sup> ont la basse en commun, et que l'on peut passer de l'un à l'autre par un mouvement conjoint sur les autres notes). {{clear}} ==== Demi-cadence ==== [[Fichier:Au clair de le lune demi cadence.midi|thumb|''Au clair de la lune'', harmonisé avec une demi-cadence.]] Une demi-cadence est une phrase ou un morceau se concluant sur l'accord construit sur le cinquième degré. Il provoque une sensation d'attente, de suspens. Il s'agit en général d'une succession {{Times New Roman|II - V}} ou {{Times New Roman|IV - V}}. C'est une cadence suspensive. On utilise rarement un accord de septième de dominante. {{clear}} ==== Cadence rompue ou évitée ==== La cadence rompue, ou cadence évitée, est succession d'un accord de dominante et d'un accord de sus-dominante, {{Times New Roman|V}} - {{Times New Roman|VI}}. C'est une cadence suspensive. ==== Cadence imparfaite ==== Une cadence imparfaite est une cadence {{Times New Roman|V - I}}, comme la cadence parfaite, mais dont au moins un des deux accords est dans un état renversé. ==== Cadence plagale ==== La cadence plagale — du grec ''plagios'', oblique, en biais — est la succession de l'accord construit sur le quatrième degré, suivi de l'accord parfait : {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}. Elle peut être utilisée après une cadence parfaite ({{Times New Roman|V<sup>5</sup> - I<sup>5</sup>}} - {{Times New Roman|IV<sup>5</sup> - I<sup>5</sup>}}). Elle donne un caractère solennel, voire religieux — elle est parfois appelée « cadence amen » —, elle a un côté antique qui rappelle la musique modale et médiévale<ref>{{lien web |url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/la-cadence-amen-ou-comment-se-dire-adieu-7191921 |titre=La cadence « Amen » ou comment se dire adieu |auteur=Max Dozolme (MAXXI Classique) |site=France Musique |date=2025-04-25 |consulté le=2025-04-25}}.</ref>. C'est une cadence conclusive. ==== {{lang|en|Turnaround}} ==== [[Fichier:Au clair de le lune turnaround.midi|thumb|Au clair de la lune, harmonisé en style jazz : accords de 7{{e}}, anatole suivie d'un ''{{lang|en|turnaround}}'' ii-V-I.]] Le terme ''{{lang|en|turnaround}}'' signifie revirement, retournement. C'est une succession d'accords que fait la transition entre deux parties, en créant une tension-résolution. Le ''{{lang|en|turnaround}}'' le plus courant est la succession {{Times New Roman|II - V - I}}. On utilise également fréquemment l'anatole : {{Times New Roman|I - VI - II - V}}. {| class="wikitable" |+ Progressions typiques d'accords dans une tonalité majeure ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Cadence<br />parfaite<br /> {{Times New Roman|V - I}} ! scope="col" | Cadence<br />italienne<br />{{Times New Roman|IV - V - I}} ! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou IV - V}} ! scope="col" | Cadence<br />plagale<br />{{Times New Roman|IV - I}} ! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}} ! scope="col" | Anatole<br />{{Times New Roman|I - vi - ii - V}} |- |''Do'' majeur || || G - C || F - G - C || Dm - G ou F - G || F - C || Dm - G - C || C - Am - Dm - G |- |''Sol'' majeur || ''fa''♯ || D - G || C - D - G || Am - D ou C - D || C - G || Am - D - G || G - Em - Am - D |- |''Ré'' majeur || ''fa''♯, ''do''♯ || A - D || G - A - D || Em - A ou G - A || G - D || Em - A - D || D - Bm - Em - A |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || E - A || D - E - A || Bm - E ou D - E || D - A || Bm - E - A || A - F♯m - B - E |- | ''Fa'' majeur || ''si''♭ || C - F || B♭ - C - F || Gm - C ou B♭ - C || B♭ - F || Gm - C - F || F - Dm - Gm - C |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || F - B♭ || E♭ - F - B♭ || Cm - F ou E♭ - F || E♭ - B♭ || Cm - F - B♭ || B♭ - Gm - Cm - F |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || B♭ - E♭ || A♭ - B♭ - E♭ || Fm - B♭ ou A♭ - B♭ || A♭ - E♭ || Fm - B♭ - E♭ || Gm - Cm - Fm - B♭ |} {| class="wikitable" |+ Progressions typiques d'accords dans une tonalité mineure ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" | Cadence<br />parfaite<br />{{Times New Roman|V - i}} ! scope="col" | Cadence<br />italienne<br />{{Times New Roman|iv - V - i}} ! scope="col" | Demi-<br />cadence<br />{{Times New Roman|ii - V ou iv - V}} ! scope="col" | Cadence<br />plagale<br />{{Times New Roman|iv - i}} ! scope="col" | ''Turnaround''<br />{{Times New Roman|ii - V - I}} ! scope="col" | Anatole<br />{{Times New Roman|i - VI - ii - V}} |- | ''La'' mineur<br />harmonique || || E - Am || Dm - E - Am || B° - E ou Dm - E || Dm - Am || B° - E - Am || Am - F - B° - E |- | ''Mi'' mineur<br />harmonique || ''fa''♯ || B - Em || Am - B - Em || F♯° - B ou Am - B || Am - Em || F♯° - B - Em || Em - C - F♯° - B |- | ''Si'' mineur<br />harmonique || ''fa''♯, ''do''♯ || F♯ - Bm || Em - F♯ - Bm || C♯° - F♯ ou Em - F♯ || Em - Bm || C♯° - F♯ - Bm || Bm - G - C♯° - F♯ |- | ''Fa''♯ mineur<br />harmonique || ''fa''♯, ''do''♯, ''sol''♯ || C♯ - F♯m || Bm - C♯ - F♯m || G♯° - C♯ ou Bm - C♯ || Bm - F♯m || G♯° - C♯ - F♯m || A+ - D - G♯° - C♯ |- | ''Ré'' mineur<br />harmonique || ''si''♭ || A - Dm || Gm - A - Dm || E° - A ou Gm - A || Gm - Dm || E° - A - Dm || Dm - B♭ - E° - A |- | ''Sol'' mineur<br />harmonique || ''si''♭, ''mi''♭ || D - Gm || Cm - D - Gm || A° - D ou Cm - D || Cm - Gm|| A° - D - Gm || Gm - E♭ - A° - D |- | ''Do'' mineur<br />harmonique || ''si''♭, ''mi''♭, ''la''♭ || G - Cm || Fm - G - Cm || D° - G ou Fm - G || Fm - Dm || D° - G - Cm || Cm - A♭ - D° - G |} ==== Exemple : ''La Mer'' ==== : {{lien web | url = https://www.youtube.com/watch?v=PXQh9jTwwoA | titre = Charles Trenet - La mer (Officiel) [Live Version] | site = YouTube | auteur = Charles Trenet | consulté le = 2020-12-24 }} Le début de ''La Mer'' (Charles Trenet, 1946) est en ''do'' majeur et est harmonisé par l'anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (C - Am - Dm - G<sup>7</sup>) sur deux mesures, jouée deux fois ({{Times New Roman|1=<nowiki>|I-vi|ii-V</nowiki><sup>7</sup><nowiki>|</nowiki>}} × 2). Viennent des variations avec les progressions {{Times New Roman|I-III-vi-V<sup>7</sup>}} (C - E - Am - G<sup>7</sup>) puis la « progression ’50s » (voir plus bas) {{Times New Roman|I-vi-IV-VI<sup>7</sup>}} (C - Am - F - A<sup>7</sup>, on remarque que {{Times New Roman|IV}}/F est le relatif majeur du {{Times New Roman|ii}}/Dm de l'anatole), jouées chacune une fois sur deux mesure ; puis cette première partie se conclut par une demie cadence {{Times New Roman|ii-V<sup>7</sup>}} sur une mesure puis une dernière anatole sur trois mesures ({{Times New Roman|1=<nowiki>|I-vi|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}}). Cela constitue une première partie « A » sur douze mesures qui se termine par une demi-cadence ({{Times New Roman|ii-V<sup>7</sup>}}) qui appelle une suite. Cette partie A est jouée une deuxième fois mais la fin est modifiée pour la transition : les deux dernières mesures {{Times New Roman|<nowiki>|ii|V</nowiki><sup>7</sup><nowiki>|</nowiki>}} deviennent {{Times New Roman|<nowiki>|ii-V</nowiki><sup>7</sup><nowiki>|I|</nowiki>}} (|Dm-G7|C|), cette partie « A’ » se conclut donc par une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}). Le morceau passe ensuite en tonalité de ''mi'' majeur, donc une tierce au dessus de ''do'' majeur, sur six mesures. Cette partie utilise une progression ’50s {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (E - C♯m - A - B<sup>7</sup>), qui est rappelons-le une variation de l'anatole, l'accord {{Times New Roman|ii}} (Fm) étant remplacé par son relatif majeur {{Times New Roman|IV}} (A). Cette anatole modifiée est jouée deux fois puis la partie en ''mi'' majeur se conclut par l'accord parfait {{Times New Roman|I}} joué sur deux mesures (|E|E|), on a donc, avec la mesure précédente, avec une cadence parfaite ({{Times New Roman|V<sup>7</sup>-I}}). Suivent ensuite six mesures en ''sol'' majeur, donc à nouveau une tierce au dessus de ''mi'' majeur. Elle comporte une progression {{Times New Roman|I-vi-IV-V<sup>7</sup>}} (G - Em - C - D<sup>7</sup>), donc anatole avec substitution du {{Times New Roman|ii}}/Am par son relatif majeur {{Times New Roman|VI}}/C (progression ’50s), puis une anatole {{Times New Roman|I-vi-ii-V<sup>7</sup>}} (G - Em - Am - D<sup>7</sup>) et deux mesure sur la tonique {{Times New Roman|I-I<sup>7</sup>}} (G - G<sup>7</sup>), formant à nouveau une cadence parfaite. La fin sur un accord de septième, dissonant, appelle une suite. Cette partie « B » de douze mesures comporte donc deux parties similaires « B1 » et « B2 » qui forment une marche harmonique (montée d'une tierce). Le morceau se conclut par une reprise de la partie « A’ » et se termine donc par une cadence parfaite. Nous avons une structure A-A’-B-A’ sur 48 mesures, proche la forme AABA étudiée plus loin. Donc ''La Mer'' est un morceau structuré autour de l'anatole avec des variations (progression ’50s, substitution du {{Times New Roman|ii}} par son relatif majeur {{Times New Roman|IV}}) et comportant une marche harmonique dans sa troisième partie. Les parties se concluent par des ''{{lang|en|turnarounds}}'' sous la forme d'une cadence parfaite ou, pour la partie A, par une demi-cadence. {| border="1" rules="rows" frame="hsides" |+ Structure de ''La Mer'' |- align="center" | | colspan="12" | ''do'' majeur | |- align="center" ! scope="row" rowspan=2 | A | colspan="2" | anatole | colspan="2" | // | colspan="2" | variation | colspan="2" | ’50s | ½ c. | colspan="3" | anatole | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii}} || <nowiki>|</nowiki> {{Times New Roman|V<sup>7</sup>}} || <nowiki>|</nowiki> |- align="center" ! scope="row" rowspan="2" | A’ | colspan="2" | anatole | colspan="2" | // | colspan="2" | variation | colspan="2" | ’50s | ½ c. | colspan="2" | anatole | c.p. | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-III}} || <nowiki>|</nowiki> {{Times New Roman|vi-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-VI<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|ii-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> |- align="center" | | colspan="6" | B1 : ''mi'' majeur | colspan="6" background="lightgray" | B2 : ''sol'' majeur | |- align="center" ! scope="row" rowspan="2" | B | colspan="2" | ’50s | colspan="2" | // |colspan="2" | c.p. | colspan="2" | ’50s | colspan="2" | // |colspan="2" | c.p. | |- | <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I-vi}} || <nowiki>|</nowiki> {{Times New Roman|IV-V<sup>7</sup>}} || <nowiki>|</nowiki> {{Times New Roman|I}} || <nowiki>|</nowiki> {{Times New Roman|I<sup>7</sup>}} || <nowiki>|</nowiki> |- ! scope="row" | A’ | colspan="12" | | |} === Progression blues === La musique blues est apparue dans les années 1860. Elle est en général bâtie sur une grille d'accords ''({{lang|en|changes}})'' immuable de douze mesures ''({{lang|en|twelve-bar blues}})''. C'est sur cet accompagnement qui se répète que s'ajoute la mélodie — chant et solo. Cette structure est typique du blues et se retrouve dans ses dérivés comme le rock 'n' roll. Le rythme est toujours un rythme ternaire syncopé ''({{lang|en|shuffle, swing, groove}}, ''notes inégales'')'' : la mesure est à quatre temps, mais la noire est divisée en noire-croche en triolet, ou encore triolet de croche en appuyant la première et la troisième. La mélodie se construit en général sur une gamme blues de six degrés (gamme pentatonique mineure avec une quarte augmentée), mais bien que la gamme soit mineure, l'harmonie est construite sur la gamme majeure homonyme : un blues en ''fa'' a une mélodie sur la gamme de ''fa'' mineur, mais une harmonie sur la gamme de ''fa'' majeur. La grille d'accord comporte les accords construits sur les degrés {{Times New Roman|I}}, {{Times New Roman|IV}} et {{Times New Roman|V}} de la gamme majeure homonyme. Les accords sont souvent des accords de septième (donc avec une tierce majeure et une septième mineure), il ne s'agit donc pas d'une harmonisation de gamme diatonique (puisque la septième est majeure sur l'accord de tonique). Par exemple, pour un blues en ''do'' : * accord parfait de do majeur, C ({{Times New Roman|I}}<sup>er</sup> degré) ; * accord parfait de fa majeur, F ({{Times New Roman|IV}}<sup>e</sup> degré) ; * accord parfait de sol majeur, G ({{Times New Roman|V}}<sup>e</sup> degré). Il existe quelques morceaux harmonisés avec des accords mineurs, comme par exemple ''As the Years Go Passing By'' d'Albert King (Duje Records, 1959). La progression blues est organisée en trois blocs de quatre mesures ayant les fonctions suivantes (voir ci-dessus ''[[#Harmonie fonctionnelle|Harmonie fonctionnelle]]'') : * quatre mesures toniques ; * quatre mesures sous-dominantes ; * quatre mesures dominantes. La forme la plus simple, que Jeff Gardner appelle « forme A », est la suivante : {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, forme A |- ! scope="row" | Tonique | width="50px" | I | width="50px" | I | width="50px" | I | width="50px" | I |- ! scope="row" | Sous-domminante | width="50px" | IV | width="50px" | IV | width="50px" | I | width="50px" | I |- ! scope="row" | Dominante | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | V |} La progression {{Times New Roman|I-V}} des deux dernières mesures forment le ''{{lang|en|turnaround}}'', la demie cadence qui lance le cycle suivant. Nous présentons ci-dessous un exemple typique de ligne de basse ''({{lang|en|walking bass}})'' pour le ''{{lang|en|turnaround}}'' d'un blues en ''la'' : [[Fichier:Turnaround classique blues en la.svg|Exemple typique de ligne de basse pour un ''turnaround'' de blues en ''la''.]] [[Fichier:Blues mi harmonie elementaire.midi|thumb|Blues en ''mi'', harmonisé de manière élémentaire avec une ''{{lang|en|walking bass}}''.]] Vous pouvez écouter ci-contre une harmonisation typique d'un blues en ''mi''. Les accords sont exécutés par une basse marchante ''({{lang|en|walking bass}})'', qui joue une arpège sur la triade avec l'ajout d'une sixte majeure et d'une septième mineure, et par une guitare qui joue un accord de puissance ''({{lang|en|power chord}})'', qui n'est composé que de la fondamentale et de la quinte juste, avec une sixte en appoggiature. La forme B s'obtient en changeant la deuxième mesure : on joue un degré {{Times New Roman|IV}} au lieu d'un degré {{Times New Roman|I}}. La progression {{Times New Roman|I-IV}} sur les deux premières mesures est appelé ''{{lang|en|quick change}}''. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, forme B |- | width="50px" | I | width="50px" | IV | width="50px" | I | width="50px" | I |- | width="50px" | IV | width="50px" | IV | width="50px" | I | width="50px" | I |- | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | V |} Par exemple, ''Sweet Home Chicago'' (Robert Johnson, 1936) est un blues en ''fa'' ; sa grille d'accords, aux variations près, suit une forme B : {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression de ''Sweet Home Chicago'' |- | width="50px" | F | width="50px" | B♭ | width="50px" | F | width="50px" | F |- | width="50px" | B♭ | width="50px" | B♭ | width="50px" | F | width="50px" | F |- | width="50px" | C7 | width="50px" | B♭7 | width="50px" | F7 | width="50px" | C7 |} : Écouter {{lien web | url =https://www.youtube.com/watch?v=dkftesK2dck | titre = Robert Johnson "Sweet Home Chicago" | auteur = Michal Angel | site = YouTube | date = 2007-12-09 | consulté le = 2020-12-17 }} Les formes C et D s'obtiennent à partir des formes A et B en changeant le dernier accord par un accord sur le degré {{Times New Roman|I}}, ce qui forme une cadence plagale. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Progression blues, formes C et D |- | colspan="4" | … |- | colspan="4" | … |- | width="50px" | V | width="50px" | IV | width="50px" | I | width="50px" | I |} L'harmonie peut être enrichie, notamment en jazz. Voici par exemple une grille du blues souvent utilisés en bebop. {| class="wikitable" style="font-family:Times New Roman; text-align:center;" |+ Exemple de progression de blues bebop sur une base de forme B |- | width="60px" | I<sup>7</sup> | width="60px" | IV<sup>7</sup> | width="60px" | I<sup>7</sup> | width="60px" | V–<sup>7</sup> <nowiki>|</nowiki> I<sup>7</sup> |- | width="60px" | IV<sup>7</sup> | width="60px" | IV<sup>7</sup> | width="60px" | I<sup>7</sup> | width="60px" | VI<sup>7 ♯9 ♭13</sup> |- | width="60px" | II–<sup>7</sup> | width="60px" | V<sup>7</sup> | width="60px" | V<sup>7</sup> <nowiki>|</nowiki> IV<sup>7</sup> | width="60px" | II–<sup>7</sup> <nowiki>|</nowiki> V<sup>7</sup> |} On peut aussi trouver des blues sur huit mesures, sur seize mesures comme ''Watermelon Man'' de Herbie Hancock (album ''Takin' Off'', Blue Note, 1962) ou ''Let's Dance'' de Jim Lee (interprété par Chris Montez, Monogram, 1962) * {{lien web |url= https://www.dailymotion.com/video/x5iduwo |titre=Herbie Hancock - Watermelon Man (1962) |auteur=theUnforgettablesTv |site=Dailymotion |date=2003 |consulté le=2021-02-09 }} * {{lien web |url=https://www.youtube.com/watch?v=6JXshurYONc |titre=Let's Dance |auteur=Chris Montez |site=YouTube |date=2016-08-06 |consulté le=2021-02-09 }} À l'inverse, certains blues peuvent avoir une structure plus simple que les douze mesure ; par exemple ''Hoochie Coochie Man'' de Willie Dixon (interprété par Muddy Waters sous le titre ''Mannish Boy'', Chicago Blues, 1954) est construit sur un seul accord répété tout le long de la chanson. * {{lien web |url=https://www.dailymotion.com/video/x5iduwo |titre=Muddy Waters - Hoochie Coochie Man |auteur=Muddy Waters |site=Dailymotion |date=2012 | consulté le=2021-02-09 }} === Cadence andalouse === La cadence andalouse est une progression de quatre accords, descendant par mouvement conjoint : * en mode de ''mi'' (mode phrygien) : {{Times New Roman|IV}} - {{Times New Roman|III}} - {{Times New Roman|II}} - {{Times New Roman|I}} ;<br />par exemple en ''mi'' phrygien : Am - G - F - E ; en ''do'' phrygien : Fm - E♭ - D♭ - C ;<br />on notera que le degré {{Times New Roman|III}} est diésé dans l'accord final (ou bécarre s'il est bémol dans la tonalité) ; * en mode mineur : {{Times New Roman|I}} - {{Times New Roman|VII}} - {{Times New Roman|VI}} - {{Times New Roman|V}} ;<br />par exemple en ''la'' mineur : Am - G - F - E ; en ''do'' mineur : Cm - B♭ - A♭ - m ;<br />comme précédemment, on notera que le degré {{Times New Roman|VII}} est diésé dans l'accord final. === Progressions selon le cercle des quintes === [[Fichier:Cercle quintes degres tonalite majeure.svg|vignette|Cercle des quinte justes (parcouru dans le sens des aiguilles d'une montre) des degrés d'une tonalité majeure.]] La progression {{Times New Roman|V-I}} est la cadence parfaite, mais on peut aussi l'employer au milieu d'un morceau. Cette progression étant courte, sa répétition crée de la lassitude ; on peut la compléter par d'autres accords séparés d'une quinte juste, en suivant le « cercle des quintes » : {{Times New Roman|I-V-IX}}, la neuvième étant enharmonique de la seconde, on obtient {{Times New Roman|I-V-II}} ; en retournant la progression, on obtient le turnaround {{Times New Roman|II-V-I}}. En jazz, dans le morceau ''Topsy'' (Edgar Battle et Eddir Durham), la partie <span style="border-color: black; border-style: solid; border-width: thin;">C</span> est composée d'une progression en quintes descendantes d'accords de septième de dominante : D<sup>7</sup> - G<sup>7</sup> - C<sup>7</sup> - F<sup>7</sup> - B♭<sup>7</sup> ; les improvisatiosn sur cette partie peuvent donc se faire en modulant en ''sol'' (majeur ou mineur) - ''do'' (M/m) - ''fa'' (M/m) - ''si''♭ (M/m) - ''mi''♭ (M/m). On peut continuer de décrire le cercle des quintes : {{Times New Roman|I-V-II-VI}}, on obtient l'anatole dans le désordre ; on peut à l'inverse étendre les quintes vers la gauche, {{Times New Roman|IV-I-V-II-VI}}. En musique populaire, on trouve fréquemment une progression fondée sur les accord {{Times New Roman|I}}, {{Times New Roman|IV}}, {{Times New Roman|V}} et {{Times New Roman|VI}}, popularisée dans les années 1950. La « progression années 1950 », « progression ''{{lang|en|fifties ('50)}}'' » ''({{lang|en|'50s progression}})'' est dans l'ordre {{Times New Roman|I-VI-IV-V}}. On trouve aussi cette progression en musique classique. Si la tonalité est majeure, la triade sur la sus-dominante est mineure, les autres sont majeures, on notera donc souvent {{Times New Roman|I-vi-IV-V}}. On peut avoir des permutations circulaires (le dernier accord venant au début, ou vice-versa) : {{Times New Roman|vi-IV-V-I}}, {{Times New Roman|IV-V-I-vi}} et {{Times New Roman|V-I-vi-IV}}. {| class="wikitable" |+ Accords selon la tonalité ! scope="col" | Tonalité ! scope="col" | Armure ! scope="col" style="font-family:Times New Roman" | I ! scope="col" style="font-family:Times New Roman" | IV ! scope="col" style="font-family:Times New Roman" | V ! scope="col" style="font-family:Times New Roman" | vi |- |''Do'' majeur || || C || F || G || Am |- |''Sol'' majeur || ''fa''♯ || G || C || D || Em |- |''Ré'' majeur || ''fa''♯, ''do''♯ || D || G || A || Bm |- |''La'' majeur || ''fa''♯, ''do''♯, ''sol''♯ || A || D || E || F♯m |- | ''Fa'' majeur || ''si''♭ || F || B♭ || C || Dm |- | ''Si''♭ majeur || ''si''♭, ''mi''♭ || B♭ || E♭ || F || Gm |- | ''Mi''♭ majeur || ''si''♭, ''mi''♭, ''la''♭ || E♭ || A♭ || B♭ || Cm |} Par exemple, en tonalité de ''do'' majeur, la progression {{Times New Roman|I-vi-IV-V}} sera C-Am-F-G. Il existe d'autres progressions utilisant ces accords mais dans un autre ordre, typiquement {{Times New Roman|I–IV–vi–V}} ou une de ses permutations circulaires : {{Times New Roman|IV–vi–V-I}}, {{Times New Roman|vi–V-I-IV}} ou {{Times New Roman|V-I-IV-vi}}. Ou dans un autre ordre. PV Nova l'illustre dans plusieurs de ses « expériences » dans la version {{Times New Roman|vi-V-IV-I}}, soit Am-G-F-C, ou encore {{Times New Roman|vi-IV-I-V}}, soit Am-F-C-G : : {{lien web | url = https://www.youtube.com/watch?v=w08LeZGbXq4 | titre = Expérience n<sup>o</sup> 6 — La Happy Pop | auteur = PV Nova | site = YouTube | date = 2011-08-20 | consulté le = 2020-12-13 }} et cela devient un gag récurrent avec son « chapeau des accords magiques qu'on nous ressort à toutes les sauces » : {{lien web | url = https://www.youtube.com/watch?v=VMY_vc4nZAU | titre = Expérience n<sup>o</sup> 14 — La Soupe dou Brasil | auteur = PV Nova | site = YouTube | date = 2012-10-03 | consulté le = 2020-12-17 }} Cette récurrence est également parodiée par le groupe The Axis of Awesome avec ses « chansons à quatre accords » ''({{lang|en|four-chords song}})'', dans une sketch où ils mêlent 47 chansons en utilisant l'ordre {{Times New Roman|I-V-vi-IV}} : : {{lien web | url = https://www.youtube.com/watch?v=oOlDewpCfZQ | titre = 4 Chords | Music Videos | The Axis Of Awesome | auteur = The Axis of Awesome | site = YouTube | date = 2011-07-20 | consulté le = 2020-12-17 }} {{boîte déroulante/début|titre=Chansons mêlées dans le sketch}} # Journey : ''Don't Stop Believing'' ; # James Blunt : ''You're Beautiful'' ; # Black Eyed Peas : ''Where Is the Love'' ; # Alphaville : ''Forever Young'' ; # Jason Mraz : ''I'm Yours'' ; # Train : ''Hey Soul Sister'' ; # The Calling : ''Wherever You Will Go'' ; # Elton John : ''Can You Feel The Love Tonight'' (''Le Roi lion'') ; # Akon : ''Don't Matter'' ; # John Denver : ''Take Me Home, Country Roads'' ; # Lady Gaga : ''Paparazzi'' ; # U2 : ''With Or Without You'' ; # The Last Goodnight : ''Pictures of You'' ; # Maroon Five : ''She Will Be Loved'' ; # The Beatles : ''Let It Be'' ; # Bob Marley : ''No Woman No Cry'' ; # Marcy Playground : ''Sex and Candy'' ; # Men At Work : ''Land Down Under'' ; # thème de ''America's Funniest Home Videos'' (équivalent des émissions ''Vidéo Gag'' et ''Drôle de vidéo'') ; # Jack Johnson : ''Taylor'' ; # Spice Girls : ''Two Become One'' ; # A Ha : ''Take On Me'' ; # Green Day : ''When I Come Around'' ; # Eagle Eye Cherry : ''Save Tonight'' ; # Toto : ''Africa'' ; # Beyonce : ''If I Were A Boy'' ; # Kelly Clarkson : ''Behind These Hazel Eyes'' ; # Jason DeRulo : ''In My Head'' ; # The Smashing Pumpkins : ''Bullet With Butterfly Wings'' ; # Joan Osborne : ''One Of Us'' ; # Avril Lavigne : ''Complicated'' ; # The Offspring : ''Self Esteem'' ; # The Offspring : ''You're Gonna Go Far Kid'' ; # Akon : ''Beautiful'' ; # Timberland featuring OneRepublic : ''Apologize'' ; # Eminem featuring Rihanna : ''Love the Way You Lie'' ; # Bon Jovi : ''It's My Life'' ; # Lady Gaga : ''Pokerface'' ; # Aqua : ''Barbie Girl'' ; # Red Hot Chili Peppers : ''Otherside'' ; # The Gregory Brothers : ''Double Rainbow'' ; # MGMT : ''Kids'' ; # Andrea Bocelli : ''Time To Say Goodbye'' ; # Robert Burns : ''Auld Lang Syne'' ; # Five for fighting : ''Superman'' ; # The Axis of Awesome : ''Birdplane'' ; # Missy Higgins : ''Scar''. {{boîte déroulante/fin}} Vous pouvez par exemple jouer les accords C-G-Am-F ({{Times New Roman|I-V-vi-IV}}) et chanter dessus ''{{lang|en|Let It Be}}'' (Paul McCartney, The Beattles, 1970) ou ''Libérée, délivrée'' (Robert Lopez, ''La Reine des neiges'', 2013). La progression {{Times New Roman|I-V-vi-IV}} est considérée comme « optimiste » tandis que sa variante {{Times New Roman|iv-IV-I-V}} est considérée comme « pessimiste ». On peut voir la progression {{Times New Roman|I-vi-IV-V}} comme une variante de l'anatole {{Times New Roman|I-vi-ii-V}}, obtenue en remplaçant l'accord de sustonique {{Times New Roman|ii}} par l'accord de sous-dominante {{Times New Roman|IV}} (son relatif majeur, et degré ayant la même fonction). ==== Exemples de progression selon le cercle des quintes en musique classique ==== [[Fichier:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.midi|vignette|Dietrich Buxtehude, Psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]] Cette progression selon la cercle des quintes, sous la forme {{Times New Roman|I-vi-IV-V}}, apparaît déjà au {{pc|xvii}}<sup>e</sup> siècle dans le psaume 42 ''Quem ad modum desiderat cervis'' (BuxVW92) de Dietrich Buxtehude (1637-1707). Le morceau est en ''fa'' majeur, la progression d'accords est donc F-Dm-B♭-C. : {{lien web | url = https://www.youtube.com/watch?v=8FmV9l1RqSg | titre = D. Buxtehude - Quemadmodum desiderat cervus, BuxWV 92 | auteur = Longobardo | site = YouTube | date = 2013-04-06 | consulté la = 2021-01-01 }} [[File:BuxWV92 quemadmodum desiderat cervis Dietrich Buxtehude.svg|vignette|450x450px|center|Dietrich Buxtehude, psaume 42 ''Quemadmodum desiderat cervis'', quatre premières mesures.]] {{clear}} [[Fichier:JSBach BWV140 cantate 4 mesures.midi|vignette|J.-S. Bach, cantate BWV140, quatre premières mesures.]] On la trouve également dans l'ouverture de la cantate ''{{lang|de|Wachet auf, ruft uns die Stimme}}'' de Jean-Sébastien Bach (BWV140, 1731). Le morceau est en ''mi''♭ majeur, la progression d'accords est donc E♭-Cm-A♭<sup>6</sup>-B♭. [[Fichier:JSBach BWV140 cantate 4 mesures.svg|vignette|center|J.-S. Bach, cantate BWV140, quatre premières mesures.|alt=|517x517px]] {{clear}} [[Fichier:Mozart K310 Sonate8 mesures 45 a 49.midi|vignette|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]] La même progression est utilisée par Mozart, par exemple dans le premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778), la progression d'accords est C-Am-F-G qui correspond à la progression {{Times New Roman|III-i-VI-VII}} de ''la'' mineur, mais à la progression {{Times New Roman|I-vi-IV-V}} de la gamme relative, ''do'' majeur . [[Fichier:Mozart K310 Sonate8 mesures 45 a 49.svg|vignette|center|500px|Mozart, mesures 45 à 49 du premier mouvement de la sonate pour piano n<sup>o</sup> 8 en ''la'' mineur (K310, 1778).]] === Substitution tritonique === Un des accords les plus utilisés est donc l'accord de septième de dominante, {{Times New Roman|V<sup>7</sup><sub>+</sub>}} qui contient les degrés {{Times New Roman|V}}, {{Times New Roman|VII}}, {{Times New Roman|II}} ({{Times New Roman|IX}}) et {{Times New Roman|IV}}({{Times New Roman|XI}}) ; par exemple, en tonalité de ''do'' majeur, l'accord de ''sol'' septième (G<sup>7</sup>) contient les notes ''sol''-''si''-''ré''-''fa''. Si l'on prend l'accord dont la fondamentale est trois tons (triton) au-dessus ou en dessous — l'octave contenant six tons, on arrive sur la même note —, {{Times New Roman|♭II<sup>7</sup>}}, ici ''ré''♭ septième (D♭<sup>7</sup>), celui-ci contient les notes ''ré''♭-''fa''-''la''♭-''do''♭, cette dernière note étant l'enharmonique de ''si''. Les deux accords G<sup>7</sup> et D♭<sup>7</sup> ont donc deux notes en commun : le ''fa'' et le ''si''/''do''♭. Il est donc fréquent en jazz de substituer l'accord {{Times New Roman|V<sup>7</sup><sub>+</sub>}} par l'accord {{Times New Roman|♭II<sup>7</sup>}}. Par exemple, la progression {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|V<sup>7</sup>}}-{{Times New Roman|I<sup>&Delta;</sup>}} devient {{Times New Roman|ii<sup>7</sup>}}-{{Times New Roman|♭II<sup>7</sup>}}-{{Times New Roman|I<sup>Δ</sup>}}. C'est un procédé courant de réharmonisation (le fait de remplacer un accord par un autre dans un morceau existant). Les six substitutions possibles sont donc : C<sup>7</sup>↔F♯<sup>7</sup> - D♭<sup>7</sup>↔G<sup>7</sup> - D<sup>7</sup>↔A♭<sup>7</sup> - E♭<sup>7</sup>↔A<sup>7</sup> - E<sup>7</sup>↔B♭<sup>7</sup> - F<sup>7</sup>↔B<sup>7</sup>. [[Fichier:Übermäsiger Terzquartakkord.jpg|vignette|Exemple de cadence parfaite en ''do'' majeur avec substitution tritonique (sixte française).]] Dans l'accord D♭<sup>7</sup>, si l'on remplace le ''do''♭ par son ''si'' enharmonique, on obtient un accord de sixte augmentée : ''ré''♭-''fa''-''la''♭-''si''. Cet accord est utilisé en musique classique depuis la Renaissance ; on distingue en fait trois accords de sixte augmentée : * sixte française ''ré''♭-''fa''-''sol''-''si'' ; * sixte allemande : ''ré''♭-''fa''-''la''♭-''si'' ; * sixte italienne : ''ré''♭-''fa''-''si''. Par exemple, le ''Quintuor en ''ut'' majeur'' de Franz Schubert (1828) se termine par une cadence parfaite dont l'accord de dominante est remplacé par une sixte française ''ré''♭-''fa''-''si''-''sol''-''si'' (''ré''♭ aux violoncelles, ''fa'' à l'alto, ''si''-''sol'' aux seconds violons et ''si'' au premier violon). [[Fichier:Schubert C major Quintet ending.wav|vignette|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]] [[Fichier:Schubert C major Quintet ending.png|vignette|center|upright=2.5|Sept dernières mesures du ''Quintuor en ''ut'' majeur'' de Franz Schubert.]] === Autres accords de substitution === Substituer un accord consiste à utiliser un accord provenant d'une tonalité étrangère à la tonalité en cours. À la différence d'une modulation, la substitution est très courte et ne donne pas l'impression de changer de tonalité ; on a juste un sentiment « étrange » passager. Un court passage dans une autre tonalité est également appelée « emprunt ». Nous avons déjà vu plusieurs méthodes de substitution : * utilisation d'une note étrangère : une note étrangère — note de passage, appoggiature, anticipation, retard… — crée momentanément un accord hors tonalité ; en musique classique, ceci n'est pas considéré comme un accord en propre, mais en jazz, on parle « d'accord de passage » et « d'accord suspendu » ; * utilisation d'une dominante secondaire : l'accord de dominante secondaire est hors tonalité ; le but ici est de faire une cadence parfaite, mais sur un autre degré que la tonique de la tonalité en cours ; * la substitution tritonique, vue ci-dessus, pour remplacer un accord de septième de dominante. Une dernière méthode consiste à remplacer un accord par un accord d'une gamme de même tonique, mais d'un autre mode ; on « emprunte » ''({{lang|en|borrow}})'' l'accord d'un autre mode. Par exemple, substituer un accord de la tonalité de ''do'' majeur par un accord de la tonalité de ''do'' mineur ou de ''do'' mode de ''mi'' (phrygien). Donc en ''do'' majeur, on peut remplacer un accord de ''ré'' mineur septième (D<sub>m</sub><sup>7</sup>) par un accord de ''ré'' demi-diminué (D<sup>⌀</sup>, D<sub>m</sub><sup>7&flat;5</sup>) qui est un accord appartenant à la donalité de ''la'' mineur harmonique. === Forme AABA === La forme AABA est composée de deux progressions de huit mesures, notées A et B ; cela représente trente-deux mesures au total, on parle donc souvent en anglais de la ''{{lang|en|32-bars form}}''. C'est une forme que l'on retrouve dans de nombreuses chanson de comédies musicales de Broadway comme ''Have You Met Miss Jones'' (''{{lang|en|I'd Rather Be Right}}'', 1937), ''{{lang|en|Over the Rainbow}}'' (''Le Magicien d'Oz'', Harold Harlen, 1939), ''{{lang|en|All the Things You Are}}'' (''{{lang|en|Very Warm for may}}'', 1939). Par exemple, la version de ''{{lang|en|Over the Rainbow}}'' chantée par Judy Garland est en ''la''♭ majeur et la progression d'accords est globalement : * A (couplet) : A♭-Fm | Cm-A♭ | D♭ | Cm-A♭ | D♭ | D♭-F | B♭-E♭ | A♭ * B (pont) : A♭ | B♭m | Cm | D♭ | A♭ | B♭-G | Cm-G | B♭m-E♭ soit en degrés : * A : {{Times New Roman|<nowiki>I-vi | iii-I | IV | iii-IV | IV | IV-vi | II-V | I</nowiki>}} * B : {{Times New Roman|<nowiki>I | ii | iii | IV | I | II-VII | iii-VII | ii-V</nowiki>}} Par rapport aux paroles de la chanson, on a * A : couplet 1 ''« {{lang|en|Somewhere […] lullaby}} »'' ; * A : couplet 2 ''« {{lang|en|Somewhere […] really do come true}} »'' ; * B : pont ''« {{lang|en|Someday […] you'll find me}} »'' ; * A : couplet 3 ''« {{lang|en|Somewhere […] oh why can't I?}} »'' ; : {{lien web | url = https://www.youtube.com/watch?v=1HRa4X07jdE | titre = Judy Garland - Over The Rainbow (Subtitles) | site = YouTube | auteur = Overtherainbow | consulté le = 2020-12-17 }} Une mise en œuvre de la forme AABA couramment utilisée en jazz est la forme anatole (à le pas confondre avec la succession d'accords du même nom), en anglais ''{{lang|en|rythm changes}}'' car elle s'inspire du morceau ''{{lang|en|I Got the Rythm}}'' de George Gerschwin (''Girl Crazy'', 1930) : * A : {{Times New Roman|I–vi–ii–V}} (succession d'accords « anatole ») ; * B : {{Times New Roman|III<sup>7</sup>–VI<sup>7</sup>–II<sup>7</sup>–V<sup>7</sup>}} (les fondamentales forment une succession de quartes, donc parcourent le « cercle des quintes » à l'envers). Par exemple, ''I Got the Rythm'' étant en ''ré''♭ majeur, la forme est : * A : D♭ - B♭m - E♭m - A♭ * B : F7 - B♭7 - E♭7 - A♭7 === Exemples === ==== Début du Largo de la symphonie du Nouveau Monde ==== [[File:Largo nouveau monde 5 1res mesures.svg|vignette|Partition avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]] [[File:Largo nouveau monde 5 1res mesures.midi|vignette|Fichier son avec les cinq premières mesures du Largo de la symphonie du Nouveau Monde.]] Nous avons reproduit ci-contre les cinq premières mesure du deuxième mouvement Largo de la symphonie « Du Nouveau Monde » (symphonie n<sup>o</sup> 9 d'Antonín Dvořák, 1893). Cliquez sur l'image pour l'agrandir. Vous pouvez écouter cette partie jouée par un orchestre symphonique : * {{lien web |url =https://www.youtube.com/watch?v=y2Nw9r-F_yQ?t=565 |titre = Dvorak Symphony No.9 "From the New World" Karajan 1966 |site=YouTube (Seokjin Yoon) |consulté le=2020-12-11 }} (à 9 min 25), par le Berliner Philharmoniker, dirigé par Herbert von Karajan (1966) ; * {{lien web |url = https://www.youtube.com/watch?v=ASlch7R1Zvo |titre=Dvořák: Symphony №9, "From The New World" - II - Largo |site=YouTube (diesillamusicae) |consulté le=2020-12-11 }} : Wiener Philharmoniker, dirigé par Herbert von Karajan (1985). {{clear}} Cette partie fait intervenir onze instruments monodiques (ne jouant qu'une note à la fois) : des vents (trois bois, sept cuivres) et une percussion. Certains de ces instruments sont transpositeurs (les notes sur la partition ne sont pas les notes entendues). Jouées ensemble, ces onze lignes mélodiques forment des accords. Pour étudier cette partition, nous réécrivons les parties des instruments transpositeurs en ''do'' et les parties en clef d’''ut'' en clef de ''fa''. Nous regroupons les parties en clef de ''fa'' d'un côté et les parties en clef de ''sol'' d'un autre. {{boîte déroulante|Résultat|contenu=[[File:Largo nouveau monde 5 1res mesures transpositeurs en do.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde, en do.]]}} Nous pouvons alors tout regrouper sous la forme d'un système de deux portées clef de ''fa'' et clef de ''sol'', comme une partition de piano. {{boîte déroulante|Résultat|contenu= [[Fichier:Largo nouveau monde 5 1res mesures accords.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords.]] }} [[Fichier:Largo nouveau monde 5 1res mesures accords.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords.]] {{clear}} Ensuite, nous ne gardons que la basse et les notes médium. Nous changeons éventuellement certaines notes d'octave afin de n'avoir que des superpositions de tierce ou de quinte (état fondamental des accords, en faisant ressortir les notes manquantes). {{boîte déroulante|Résultat|contenu= [[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.svg|class=transparent|center|Le début du Largo de la symphonie Du Nouveau Monde sous forme d'accords simplifiés.]] }} [[Fichier:Largo nouveau monde 5 1res mesures accords simplifies.midi|vignette|Début du Largo de la symphonie Du Nouveau Monde joué sous forme d'accords simplifiés.]] Vous pouvez écouter cette partie jouée par un quintuor de cuivres (trompette, bugle, cor, trombone, tuba), donc avec des accords de cinq notes : : {{lien web |url=https://www.youtube.com/watch?v=pWfe60nbvjA |titre = Largo from The New World Symphony by Dvorak |site=YouTube (The Chamberlain Brass) |consulté le=2020-12-11 }} : The American Academy of Arts & Letters in New York City (2017). Nous allons maintenant chiffrer les accords. Pour établir la basse chiffrée, il nous faut déterminer le parcours harmonique. Pour le premier accord, les tonalités les plus simples avec un ''sol'' dièse sont ''la'' majeur et ''fa'' dièse mineur ; comme le ''mi'' est bécarre, nous retenons ''la'' majeur, il s'agit donc d'un accord de quinte sur la dominante (les accords de dominante étant très utilisés, cela nous conforte dans notre choix). Puis nous avons un ''si'' bémol, nous pouvons être en ''fa'' majeur ou en ''ré'' mineur ; nous retenons ''fa'' majeur, c'est donc le renversement d'un accord sur le degré {{Times New Roman|II}}. Dans la deuxième mesure, nous revenons en ''la'' majeur, puis, avec un ''la'' et un ''ré'' bémols, nous sommes en ''la'' bémol majeur ; nous avons donc un accord de neuvième incomplet sur la sensible, ou un accord de onzième incomplet sur la dominante. Dans la troisième mesure, nous passons en ''ré'' majeur, avec un accord de dominante. Puis, nous arrivons dans la tonalité principale, avec le renversement d'un accord de dominante sans tierce suivi d'un accord de tonique. Nous avons donc une cadence parfaite, conclusion logique d'une phrase. La progression des accords est donc : {| class="wikitable" ! scope="row" | Tonalité | ''la'' M - ''fa'' M || ''la'' M - ''la''♭ M || ''ré'' M - ''ré''♭ M || ''ré''♭ M |- ! scope="row" | Accords | {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|II}}<sup>6</sup><sub>4</sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|“V”}}<sup>9</sup><sub><s>5</s></sub> || {{Times New Roman|V}}<sup>5</sup> - {{Times New Roman|V}}<sup>+4</sup> || {{Times New Roman|I}}<sup>5</sup> |} Dans le chiffrage jazz, nous avons donc : * une triade de ''mi'' majeur, E ; * une triade de ''sol'' majeur avec un ''ré'' en basse : G/D ; * à nouveau un E ; * un accord de ''sol'' neuvième diminué incomplet, avec un ''ré'' bémol en basse : G dim<sup>9</sup>/D♭ ; * un accord de ''la'' majeur, A ; * un accord de ''la'' bémol septième avec une ''sol'' bémol à la basse : A♭<sup>7</sup>/G♭ ; * la partie se conclue par un accord parfait de ''ré''♭ majeur, D♭. Soit une progression E - G/D | E - G dim<sup>9</sup>/D♭ | A - A♭<sup>7</sup>/G♭ | D♭. [[Fichier:Largo nouveau monde 5 1res mesures accords chiffres.svg|class=transparent|center|Début du Largo de la symphonie Du Nouveau Monde en accords simplifiés.]] {{clear}} ==== Thème de Smoke on the Water ==== Le morceau ''Smoke on the Water'' du groupe Deep Purple (album ''Machine Head'', 1972) possède un célèbre thème, un riff ''({{lang|en|rythmic figure}})'', joué à la guitare sous forme d'accords de puissance ''({{lang|en|power chords}})'', c'est-à-dire des accords sans tierce. Le morceau est en tonalité de ''sol'' mineur naturel (donc avec un ''fa''♮) avec ajout de la note bleue (''{{lang|en|blue note}}'', quinte diminuée, ''ré''♭), et les accords composant le thème sont G<sup>5</sup>, B♭<sup>5</sup>, C<sup>5</sup> et D♭<sup>5</sup>, ce dernier accord étant l'accord sur la note bleue et pouvant être considéré comme une appoggiature (indiqué entre parenthèse ci-après). On a donc ''a priori'', sur les deux premières mesures, une progression {{Times New Roman|I-III-IV}} puis {{Times New Roman|I-III-(♭V)-IV}}. Durant la majeure partie du thème, la guitare basse tient la note ''sol'' en pédale. {{note|En jazz, la qualité « <sup>5</sup> » indique que l'on n'a que la quinte (et donc pas la tierce), contrairement à la notation de basse chiffrée.}} : {{lien web | url = https://www.dailymotion.com/video/x5ili04 | titre = Deep Purple — Smoke on the Water (Live at Montreux 2006) | auteur = Deep Purple | site = Dailymotion | date = 2016 | consulté le = 2020-12-31 }} Cependant, cette progression forme une mélodie, on peut donc plus la voir comme un contrepoint, la superposition de deux voies ayant un mouvement conjoint, joué par un seul instrument, la guitare, la voie 2 étant jouée une quarte juste en dessous de la voie 1 (la quarte juste descendante étant le renversement de la quinte juste ascendante) : * voie 1 (aigu) : | ''sol'' - ''si''♭ - ''do'' | ''sol'' - ''si''♭ - (''ré''♭) - ''do'' | ; * voie 2 (grave) : | ''ré'' - ''fa'' - ''sol'' | ''ré'' - ''fa'' - (''la''♭) - ''sol'' |. En se basant sur la basse (''sol'' en pédale), nous pouvons considérer que ces deux mesures sont accompagnées d'un accord de Gm<sup>7</sup> (''sol''-''si''♭-''ré''-''fa''), chaque accord de la mélodie comprenant à chaque fois au moins une note de cet accord à l'exception de l'appogiature. {| class="wikitable" |+ Mise en évidence des notes de l'accord Gm<sup>7</sup> |- ! scope="row" | Accords | G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> |- ! scope="row" | Voie 1 | '''''sol''''' || '''''si''♭''' || ''do'' |- ! scope="row" | Voie 2 | '''''ré''''' || '''''fa''''' || '''''sol''''' |- ! scope="row" | Basse | '''''sol''''' || '''''sol''''' || '''''sol''''' |} Sur les deux mesures suivantes, la basse varie et suit les accords de la guitare avec un retard sur le dernier accord : {| class="wikitable" |+ Voies sur les mesure 3-4 du thème |- ! scope="row" | Accords | G<sup>5</sup> || B♭<sup>5</sup> || C<sup>5</sup> || B♭<sup>5</sup> || G<sup>5</sup> |- ! scope="row" | Voie 1 | ''sol'' || ''si''♭ || ''do'' || ''si''♭ || ''sol'' |- ! scope="row" | Voie 2 | ''ré'' || ''fa'' || ''sol'' || ''fa'' || ''ré'' |- ! scope="row" | Basse | ''sol'' || ''sol'' || ''do'' || ''si''♭ || ''si''♭-''sol'' |} Le couplet de cette chanson est aussi organisé sur une progression de quatre mesures, la guitare faisant des arpèges sur les accords G<sup>5</sup> (''sol''-''ré''-''sol'') et F<sup>5</sup> (''fa''-''do''-''fa'') : : | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | G<sup>5</sup>-F<sup>5</sup> | G<sup>5</sup>-G<sup>5</sup> | soit une progression {{Times New Roman|<nowiki>| I-I | I-I | I-VII | I-I |</nowiki>}}. Nous pouvons aussi harmoniser le riff du thème sur cette progression, avec un accord F (''fa''-''la''-''do'') ; nous pouvons aussi nous rappeler que l'accord sur le degré {{Times New Roman|VII}} est plus volontiers considéré comme un accord de septième de dominante {{Times New Roman|V<sup>7</sup>}}, soit ici un accord Dm<sup>7</sup> (''ré''-''fa''-''la''-''do''). On peut donc considérer la progression harmonique sur le thème : : | Gm-Gm | Gm-Gm | Gm-F ou Dm<sup>7</sup> | Gm-Gm |. Cette analyse permet de proposer une harmonisation enrichie du morceau, tout en se rappelant qu'une des forces du morceau initial est justement la simplicité de sa structure, qui fait ressortir la virtuosité des musiciens. Nous pouvons ainsi comparer la version album à la version concert avec orchestre ou à la version latino de Pat Boone. À l'inverse, le groupe Psychostrip, dans une version grunge, a remplacé les accords par une ligne mélodique : * le thème ne contient plus qu'une seule voie (la guitare ne joue pas des accords de puissance) ; * dans les mesures 9 et 10, la deuxième guitare joue en contrepoint de type mouvement inverse, qui est en fait la voie 2 jouée en miroir ; * l'arpège sur le couplet est remplacé par une ligne mélodique en ostinato sur une gamme blues. {| class="wikitable" |+ Contrepoint sur les mesures 9 et 10 |- ! scope="row" | Guitare 1 | ''sol'' ↗ ''si''♭ ↗ ''do'' |- ! scope="row" | Guitare 2 | ''sol'' ↘ ''fa'' ↘ ''ré'' |} * {{lien web | url = https://www.dailymotion.com/video/x5ik234 | titre = Deep Purple — Smoke on the Water (In Concert with the London Symphony Orchestra, 1999) | auteur = Deep Purple | site = Dailymotion | date = 2016 | consulté le = 2020-12-31 }} * {{lien web | url = https://www.youtube.com/watch?v=MtUuNzVROIg | titre = Pat Boone — Smoke on the Water (In a Metal Mood, No More Mr. Nice Guy, 1997) | auteur = Orrore a 33 Giri | site = YouTube | date = 2019-06-24 | consulté le = 2020-12-31 }} * {{lien web | url = https://www.youtube.com/watch?v=n7zLlZ8B0Bk | titre = Smoke on the Water (Heroes, 1993) | auteur = Psychostrip | site = YouTube | date = 2018-06-20 | consulté le = 2020-12-31 }} == Accords et improvisation == Nous avons vu précédemment (chapitre ''[[../Gammes et intervalles#Modes et improvisation|Gammes et intervalles &gt; Modes et improvisation]]'') que le choix d'un mode adapté permet d'improviser sur un accord. L'harmonisation des gammes permet, en inversant le processus, d'étendre notre palette : il suffit de repérer l'accord sur une harmonisaiton de gamme, et d'utiliser cette gamme-là, dans le mode correspondant du degré de l'accord (voir ci-dessus ''[[#Harmonisation par des accords de septième|Harmonisation par des accords de septième]]''). Par exemple, nous avons vu que l'accord sur le septième degré d'une gamme majeure était un accord demi-diminué ; nous savons donc que sur un accord demi-diminué, nous pouvons improviser sur le mode correspondant au septième degré, soit le mode de ''si'' (locrien). Un accord de septième de dominante étant commun aux deux tonalités homonymes (par exemple ''fa'' majeur et ''fa'' mineur pour un ''do''<sup>7</sup><sub>+</sub> / C<sup>7</sup>), nous pouvons utiliser le mode de ''sol'' de la gamme majeure (mixolydien) ou de la gamme mineure mineure (mode phrygien dominant, ou phrygien espagnol) pour improviser. Mais l'accord de septième de dominante est aussi l'accord au début d'une grille blues ; on peut donc improviser avec une gamme blues, même si la tierce est majeure dans l'accord et mineure dans la gamme. [[Fichier:Mode improvisation accords do complet.svg]] == Autres accords courants == [[fichier:Cluster cdefg.png|vignette|Agrégat ''do - ré - mi - fa - sol''.]] Nous avons vu précédemment l'harmonisation des tonalités majeures et mineures harmoniques par des triades et des accords de septième ; certains accords étant rarement utilisés (l'accord sur le degré {{Times New Roman|III}} et, pour les tonalités mineures harmoniques, l'accord sur la tonique), certains accords étant utilisés comme des accords sur un autre degré (les accords sur la sensible étant considérés comme des accords de dominante sans fondamentale). Dans l'absolu, on peut utiliser n'importe quelle combinaison de notes, jusqu'aux agrégats, ou ''{{lang|en|clusters}}'' (mot anglais signifiant « amas », « grappe ») : un ensemble de notes contigües, séparées par des intervalles de seconde. Dans la pratique, on reste souvent sur des accords composés de superpositions de tierces, sauf dans le cas de transitions (voir la section ''[[#Notes étrangères|Notes étrangère]]''). === En musique classique === On utilise parfois des accords dont les notes ne sont pas dans la tonalité (hors modulation). Il peut s'agir d'accords de passage, de notes étrangères, par exemple utilisant un chromatisme (mouvement conjoint par demi-tons). Outre les accords de passage, les autres accords que l'on rencontre couramment en musique classique sont les accords de neuvième, et les accords de onzième et treizième sur tonique. Ces accords sont simplement obtenus en continuant à empiler les tierces. Il n'y a pas d'accord d'ordre supérieur car la quinzième est deux octaves au-dessus de la fondamentale. Comme pour les accords de septième, on distingue les accords de neuvième de dominante et les accords de neuvième d'espèce. Dans le cas de la neuvième de dominante, il y a une différence entre les tonalités majeures et mineures : l'intervalle de neuvième est respectivement majeur et mineur. Les chiffrages des renversements peuvent donc différer. Comme pour les accords de septième de dominante, on considère que les accords de septième sur le degré {{Times New Roman|VI}} sont en fait des accords de neuvième de dominante sans fondamentale. Les accords de neuvième d'espèce sont en général préparés et résolus. Préparés : la neuvième étant une note dissonante (c'est à une octave près la seconde de la fondamentale), l'accord qui précède doit contenir cette note, mais dans un accord consonant ; la neuvième est donc commune avec l'accord précédent. Résolus : la dissonance est résolue en abaissant la neuvième par un mouvement conjoint. Par exemple, en tonalité de ''do'' majeur, si l'on veut utiliser un accord de neuvième d'espèce sur la tonique ''(do - mi - sol - si - ré)'', on peut utiliser avant un accord de dominante ''(sol - si - ré)'' en préparation puis un accord parfait sur le degré {{Times New Roman|IV}} ''(fa - la - do)'' en résolution ; nous avons donc sur la voie la plus aigüe la succession ''ré'' (consonant) - ''ré'' (dissonant) - ''do'' (consonant). On rencontre également parfois des accords de onzième et de treizième. On omet en général la tierce, car elle est dissonante avec la onzième. L'accord le plus fréquemment rencontré est l'accord sur la tonique : on considère alors que c'est un accord sur la dominante que l'on a enrichi « par le bas », en ajoutant une quinte inférieure. par exemple, dans la tonalité de ''do'' majeur, l'accord ''do - sol - si - ré - fa'' est considéré comme un accord de septième de dominante sur tonique, le degré étant noté « {{Times New Roman|V}}/{{Times New Roman|I}} ». De même pour l'accord ''do - sol - si - ré - fa - la'' qui est considéré comme un accord de neuvième de dominante sur tonique. === En jazz === En jazz, on utilise fréquemment l'accord de sixte à la place de l'accord de septième majeure sur la tonique. Par exemple, en ''do'' majeur, on utilise l'accord C<sup>6</sup> ''(do - mi - sol - la)'' à la place de C<sup>Δ</sup> ''(do - mi - sol - si)''. On peut noter que C<sup>6</sup> est un renversement de Am<sup>7</sup> et pourrait donc se noter Am<sup>7</sup>/C ; cependant, le fait de le noter C<sup>6</sup> indique que l'on a bien un accord sur la tonique qui s'inscrit dans la tonalité de ''do'' majeur (et non, par exemple, de ''la'' mineur naturelle) — par rapport à l'harmonie fonctionnelle, on remarquera que Am<sup>7</sup> a une fonction tonique, l'utilisation d'un renversement de Am<sup>7</sup> à la place d'un accord de C<sup>Δ</sup> est donc logique. Les accords de neuvième, onzième et treizième sont utilisés comme accords de septième enrichis. Le chiffrage suit les règles habituelles : on ajoute un « 9 », un « 11 » ou un « 13 » au chiffrage de l'accord de septième. On utilise également des accords dits « suspendus » : ce sont des accords de transition qui sont obtenus en prenant une triade majeure ou mineure et en remplaçant la tierce par la quarte juste (cas le plus fréquent) ou la seconde majeure. Plus particulièrement, lorsque l'on parle simplement « d'accord suspendu » sans plus de précision, cela désigne l'accord de neuvième avec une quarte suspendue, noté « 9sus4 » ou simplement « sus ». == L'harmonie tonale == L'harmonie tonale est un ensemble de règle assez strictes qui s'appliquent dans la musique savante européenne, de la période baroque à la période classique classique ({{pc|xiv}}<sup>e</sup>-{{pc|xviii}}<sup>e</sup> siècle). Certaines règles sont encore largement appliquées dans divers styles musicaux actuels, y compris populaire (rock, rap…), d'autres sont au contraire ignorées (par exemple, un enchaînement de plusieurs accords de même qualité forme un mouvement parallèle, ce qui est proscrit en harmonie tonale). De nos jours, on peut voir ces règles comme des règles « de bon goût », et leur application stricte comme une manière de composer « à la manière de ». Précédemment, nous avons vu la progression des accords. Ci-après, nous abordons aussi la manière dont les notes de l'accord sont réparties entre plusieurs voix, et comment on construit chaque voix. === Concepts fondamentaux === ; Consonance : Les intervalles sont considérés comme « plus ou moins consonants » : :* consonance parfaite : unisson, quinte et octave ; :* consonance mixte (parfaite dans certains contextes, imparfaite dans d'autres) : quarte ; :* consonance imparfaite : tierce et sixte ; :* dissonance : seconde et septième. ; Degrés : Certains degrés sont considérés comme « forts », « meilleurs », ce sont les « notes tonales » : {{Times New Roman|I}} (tonique), {{Times New Roman|IV}} (sous-dominante) et {{Times New Roman|V}} (dominante). [[Fichier:Mouvements harmoniques.svg|vignette|upright=0.75|Mouvements harmoniques.]] ; Mouvements : Le mouvement décrit la manière dont les voix évoluent les unes par rapport aux autres : :# Mouvement parallèle : les voix sont séparées par un intervalle constant. :# Mouvement oblique : une voix reste constante, c'est le bourdon ; l'autre monte ou descend. :# Mouvement contraire : une voix descend, l'autre monte. :# Échange de voix : les voix échangent de note ; les mélodies se croisent mais on a toujours le même intervalle harmonique. {{clear}} === Premières règles === ; Règle du plus court chemin : Quand on passe d'un accord à l'autre, la répartition des notes se fait de sorte que chaque voix fait le plus petit mouvement possible. Notamment : si les deux accords ont des notes en commun, alors les voix concernées gardent la même note. : Les deux voix les plus importantes sont la voix aigüe — soprano — et la voix la plus grave — basse. Ces deux voix sont relativement libres : la voix de soprano a la mélodie, la voix de basse fonde l'harmonie. La règle du plus court chemin s'applique surtout aux voix intermédiaires ; si l'on a des mouvements conjoints, ou du moins de petits intervalles — c'est le sens de la règle du plus court chemin —, alors les voix sont plus faciles à interpréter. Cette règle évite également que les voix n'empiètent l'une sur l'autre (voir la règle « éviter le croisement des voix »). ; Éviter les consonances parfaites consécutives :* Lorsque deux voix sont à l'unisson ou à l'octave, elles ne doivent pas garder le même intervalle, l'effet serait trop plat. :* Lorsque deux voix sont à la quarte ou à la quinte, elles ne doivent pas garder le même intervalle, car l'effet est trop dur. : Pour éviter cela, lorsque l'on part d'un intervalle juste, on a intérêt à pratiquer un mouvement contraire aux voix qui ne gardent pas la même note, ou au moins un mouvement direct : les voix vont dans le même sens, mais l'intervalle change. : Notez que même avec le mouvement contraire, on peut avoir des consonances parfaites consécutives, par exemple si une voix fait ''do'' aigu ↗ ''sol'' aigu et l'autre ''sol'' médium ↘ ''do'' grave. : L'interdiction des consonances parfaites consécutives n'a pas été toujours appliquée, le mouvement parallèle strict a d'ailleurs été le premier procédé utilisé dans la musique religieuse au {{pc|x}}<sup>e</sup> siècle. On peut par exemple utiliser des quintes parallèles pour donner un style médiéval au morceau. On peut également utiliser des octaves parallèles sur plusieurs notes afin de créer un effet de renforcement de la mélodie. : Par ailleurs, les consonances parfaites consécutives sont acceptées lorsqu'il s'agit d'une cadence (transition entre deux parties ou bien conclusion du morceau). ; Éviter le croisement des voix : Les voix sont organisées de la plus grave à la plus aigüe. Deux voix n'étant pas à l'unisson, celle qui est plus aigüe ne doit pas devenir la plus grave et ''vice versa''. ; Soigner la partie soprano : Comme c'est celle qu'on entend le mieux, c'est en général celle qui porte la mélodie principale. On lui applique des règles spécifiques : :# Si elle chante la sensible dans un accord de dominante ({{Times New Roman|V}}), alors elle doit monter à la tonique, c'est-à-dire que la note suivante sera la tonique située un demi-ton au dessus. :# Si l'on arrive à une quinte ou une octave entre les parties basse et soprano par un mouvement direct, alors sur la partie soprano, le mouvement doit être conjoint. On doit donc arriver à cette situation par des notes voisines au soprano. ; Préférer certains accords : Les deux degrés les plus importants sont la tonique ({{Times New Roman|I}}) et la dominante ({{Times New Roman|V}}), les accords correspondants ont donc une importance particulière. : À l'inverse, l'accord de sensible ({{Times New Roman|VII}}) n'est pas considéré comme ayant une fonction harmonique forte. On le considère comme un accord de dominante affaibli. En tonalité mineure, on évite également l'accord de médiante ({{Times New Roman|III}}). : Donc on utilise en priorité les accords de : :# {{Times New Roman|I}} et {{Times New Roman|V}}. :# Puis {{Times New Roman|II}}, {{Times New Roman|IV}}, {{Times New Roman|VI}} ; et {{Times New Roman|III}} en mode majeur. :# On évite {{Times New Roman|VII}} ; et {{Times New Roman|III}} en mode mineur. ; Préférer certains enchaînements : Les enchaînements d'accord peuvent être classés par ordre de préférence. Par ordre de préférence décroissante (du « meilleur » au « moins bon ») : :# Meilleurs enchaînements : quarte ascendante ou descendante. Notons que la quarte est le renversement de la quinte, on a donc des enchaînements stables et naturels, mais avec un intervalle plus court qu'un enchaînement de quintes. :# Bons enchaînements : tierce ascendante ou descendante. Les accords consécutifs ont deux notes en commun. :# Enchaînements médiocres : seconde ascendante ou descendante. Les accords sont voisins, mais ils n'ont aucune note en commun. On les utilise de préférence en mouvement ascendant, et on utilise surtout les enchaînements {{Times New Roman|IV}}-{{Times New Roman|V}}, {{Times New Roman|V}}-{{Times New Roman|VI}} et éventuellement {{Times New Roman|I}}-{{Times New Roman|II}}. :# Les autres enchaînements sont à éviter. : On peut atténuer l'effet d'un enchaînement médiocre en plaçant le second accord sur un temps faible ou bien en passant par un accord intermédiaire. [[Fichier:Progression Vplus4 I6.svg|thumb|Résolution d'un accord de triton (quarte sensible) vers l'accord de sixte de la tonique.]] ; La septième descend par mouvement conjoint : Dans un accord de septième de dominante, la septième — qui est donc le degré {{Times New Roman|IV}} — descend par mouvement conjoint — elle est donc suivie du degré {{Times New Roman|III}}. : Corolaire : un accord {{Times New Roman|V}}<sup>+4</sup> se résout par un accord {{Times New Roman|I}}<sup>6</sup> : on a bien un enchaînement {{Times New Roman|V}} → {{Times New Roman|I}}, et la 7{{e}} (degré {{Times New Roman|IV}}), qui est la basse de l'accord {{Times New Roman|V}}<sup>+4</sup>, descend d'un degré pour donner la basse de l'accord {{Times New Roman|I}}<sup>6</sup> (degré {{Times New Roman|III}}). {{clear}} [[Fichier:Progression I64 V7plus I5.svg|thumb|Accord de sixte et de quarte cadentiel.]] ; Un accord de sixte et quarte est un accord de passage : Le second renversement d'un accord parfait est soit une appoggiature, soit un accord de passage, soit un accord de broderie. : S'il s'agit de l'accord de tonique {{Times New Roman|I}}<sup>6</sup><sub>4</sub>, c'est « accord de sixte et quarte de cadence », l'appoggiature de l'accord de dominante de la cadence parfaite. {{clear}} Mais il faut appliquer ces règles avec discernement. Par exemple, la voix la plus aigüe est celle qui s'entend le mieux, c'est donc elle qui porte la mélodie principale. Il est important qu'elle reste la plus aigüe. La voix la plus grave porte l'harmonie, elle pose les accords, il est donc également important qu'elle reste la plus grave. Ceci a deux conséquences : # Ces deux voix extrêmes peuvent avoir des intervalles mélodiques importants et donc déroger à la règle du plus court chemin : la voix aigüe parce que la mélodie prime, la voix de basse parce que la progression d'accords prime. # Les croisements des voix intermédiaires sont moins critiques. Par ailleurs, si l'on applique strictement toutes les règles « meilleurs accords, meilleurs enchaînements », on produit un effet conventionnel, stéréotypé. Il est donc important d'utiliser les solutions « moins bonnes », « médiocres » pour apporter de la variété. Ajoutons que les renversements d'accords permettent d'avoir plus de souplesse : on reste sur le même accord, mais on enrichit la mélodie sur chaque voix. Le ''Bolero'' de Maurice Ravel (1928) brise un certain nombre de ces règles. Par exemple, de la mesure 39 à la mesure 59, la harpe joue des secondes. De la mesure 149 à la mesure 165, les piccolo jouent à la sixte, dans des mouvement strictement parallèle, ce qui donne d'ailleurs une sonorité étrange. À partir de la mesure 239, de nombreux instruments jouent en mouvement parallèles (piccolos, flûtes, hautbois, cor, clarinettes et violons). === Application === [[Fichier:Harmonisation possible de frere jacques exercice.svg|vignette|Exercice : harmoniser ''Frère Jacques''.]] Harmoniser ''Frère Jacques''. Nous considérons un morceau à quatre voix : basse, ténor, alto et soprano. La soprano chante la mélodie de ''Frère Jacques''. L'exercice consiste à proposer l'écriture des trois autres voix en respectant les règles énoncées ci-dessus. Pour simplifier, nous ajoutons les contraintes suivantes : * toutes les voix chantent des blanches ; * nous nous limitons aux accords de quinte (accords de trois sons composés d'une tierce et d'une quinte) sans avoir recours à leurs renversements (accords de sixte, accords de sixte et de quarte). Les notes à gauche de la portée indiquent la tessiture (ou ambitus), l'amplitude que peut chanter la voix. {{clear}} {{boîte déroulante/début|titre=Solution possible}} [[Fichier:Harmonisation possible de frere jacques solution.svg|vignette|Harmonisation possible de ''Frère Jacques'' (solution de l'exercice).]] Il n'y a pas qu'une solution possible. Le premier accord doit contenir un ''do''. Nous sommes manifestement en tonalité de ''do'' majeur, nous proposons de commencer par l'accord parfait de ''do'' majeur, I<sup>5</sup>. Le deuxième accord doit comporter un ''ré''. Si nous utilisons l'accord de quinte de ''ré'', nous allons créer une quinte parallèle. Nous pourrions utiliser un renversement, mais nous nous imposons de chercher un autre accord. Il peut s'agir de l'accord ''si''<sup>5</sup> ''(si-ré-fa)'' ou de l'accord de ''sol''<sup>5</sup> ''(sol-si-ré)''. La dernière solution permet d'utiliser l'accord de dominante qui est un accord important de la tonalité. La règle du plus court chemin imposerait le ''sol'' grave pour la partie de basse, mais cela est proche de la limite du chanteur, nous préférons passer au ''sol'' aigu, plus facile à chanter. Nous vérifions qu'il n'y a pas de quinte parallèle : l'intervalle ascendant ''do-sol'' (basse-alto) devient ''sol-si'' (3<sup>ce</sup>), l'intervalle descendant ''do-sol'' (soprano-alto) devient ''ré-si'' (3<sup>ce</sup>). De la même manière, pour le troisième accord, nous ne pouvons pas passer à un accord de ''la''<sup>5</sup> pour éviter une quinte parallèle. Nous avons le choix entre ''do''<sup>5</sup> ''(do-mi-sol)'' et ''mi''<sup>5</sup> ''(mi-sol-si)''. Nous préférons revenir à l'accord de fondamental, solution très stable (l'enchaînement {{Times New Roman|V}}-{{Times New Roman|I}} formant une cadence parfaite). Pour le quatrième accord, nous pourrions rester sur l'accord parfait de ''do'' mais cela planterait en quelque sorte la fin du morceau puisque l'on resterait sur la cadence parfaite ; or, nous connaissons le morceau et savons qu'il n'est pas fini. Nous choisissons l'accord de ''la''<sup>5</sup> qui est une sixte ascendante ({{Times New Roman|I}}-{{Times New Roman|VI}}). Nos aurions pu répartir les voix différemment. Par exemple : * alto : ''sol''-''si''-''sol''-''do'' ; * ténor : ''mi''-''ré''-''mi''-''mi''. {{boîte déroulante/fin}} [[Fichier:Harmonisation possible de frere jacques.midi|vignette|Fichier son correspondant.]] {{clear}} == Annexe == === Accords en musique classique === Un accord est un ensemble de notes jouées simultanément. Il peut s'agir : * de notes jouées par plusieurs instruments ; * de notes jouées par un même instrument : piano, clavecin, orgue, guitare, harpe (la plupart des instruments à clavier et des instruments à corde). Pour deux notes jouées simultanément, on parle d'intervalle « harmonique » (par opposition à l'intervalle « mélodique » qui concerne les notes jouées successivement). Les notes répétées à différentes octaves ne changent pas la nature de l'accord. La musique classique considère en général des empilements de tierces ; un accord de trois notes sera constitué de deux tierces successives, un accord de quatre notes de trois tierces… Lorsque tous les intervalles sont des intervalles impairs — tierces, quintes, septièmes, neuvièmes, onzièmes, treizièmes… — alors l'accord est dit « à l'état fondamental » (ou encore « primitif » ou « direct »). La note de la plus grave est appelée « fondamentale » de l'accord. Lorsque l'accord comporte un ou des intervalles pairs, l'accord est dit « renversé » ; la note la plus grave est appelée « basse ». De manière plus générale, l'accord est dit à l'état fondamental lorsque la basse est aussi la fondamentale. On a donc un état idéal de l'accord (état canonique) — un empilement strict de tierces — et l'état réel de l'accord — l'empilement des notes réellement jouées, avec d'éventuels redoublements, omissions et inversions ; et seule la basse indique si l'accord est à l'état fondamental ou renversé. Le chiffrage dit de « basse continue » ''({{lang|it|basso continuo}})'' désigne la représentation d'un accord sous la forme d'un ou plusieurs chiffres arabes et éventuellement d'un chiffre romain. ==== Accords de trois notes ==== En musique classique, les seuls accords considérés comme parfaitement consonants, c'est-à-dire sonnant agréablement à l'oreille, sont appelés « accords parfaits ». Si l'on prend une tonalité et un mode donné, alors l'accord construit par superposition es degrés I, III et V de cette gamme porte le nom de la gamme qui l'a généré. [[fichier:Accord do majeur chiffre.svg|vignette|upright=0.5|Accord parfait de ''do'' majeur chiffré.]] Par exemple : * « l'accord parfait de ''do'' majeur » est composé des notes ''do'', ''mi'' et ''sol'' ; * « l'accord parfait de ''la'' mineur » est composé des notes ''la'', ''do'' et ''mi''. Un accord parfait majeur est donc composé, en partant de la fondamentale, d'une tierce majeure et d'une quinte juste. Un accord parfait mineur est composé d'une tierce mineure et d'une quinte juste. L'accord parfait à l'état fondamental est appelé « accord de quinte » et est simplement chiffré « 5 » pour indiquer la quinte. On peut également commencer un accord sur sa deuxième ou sa troisième note, en faisant monter celle(s) qui précède(nt) à l'octave suivante. On parle alors de « renversement d'accord » ou d'accord « renversé ». [[Fichier:Accord do majeur renversements chiffre.svg|vignette|upright=0.75|Accord parfait de ''do'' majeur et ses renversements, chiffrés.]] Par exemple, * le premier renversement de l'accord parfait de ''do'' majeur est :<br /> ''mi'', ''sol'', ''do'' ; * le second renversement de l'accord parfait de do majeur est :<br /> ''sol'', ''do'', ''mi''. Les notes conservent leur nom de « fondamentale », « tierce » et « quinte » malgré le changement d'ordre. La note la plus grave est appelée « basse ». Dans le cas du premier renversement, le deuxième note est la tierce de la basse (la note la plus grave) et la troisième note est la sixte ; le chiffrage en chiffres arabes est donc « 6 » (puisque l'on omet la tierce) et l'accord est appelé « accord de sixte ». Pour le deuxième renversement, les intervalles sont la quarte et la sixte, le chiffrage est donc « 6-4 » et l'accord est appelé « accord de sixte et de quarte ». Dans tous les cas, on chiffre le degré on considérant la fondamentale, par exemple {{Times New Roman|I}} si l'accord est construit sur la tonique de la gamme. Les autres accords de trois notes que l'on rencontre sont : * l'accord de quinte diminuée, constitué d'une tierce mineure et d'une quinte diminuée ; lorsqu'il est construit sur le septième degré d'une gamme, on considère que c'est un accord de septième de dominante sans fondamentale (voir plus bas), le degré est donc indiqué « “{{Times New Roman|V}}” » (cinq entre guillemets) et non « {{Times New Roman|VII}} » ; * l'accord de quinte augmenté : il est composé d'une tierce majeure et qu'une quinte augmentée. Dans le tableau ci-dessous, * « m » désigne un intervalle mineur ; * « M » un intervalle majeur ou le mode majeur ; * « J » un intervalle juste ; * « d » un intervalle diminué ; * « A » un intervalle augmenté ; * « mh » le mode mineur harmonique ; * « ma » le mode mineur ascendant ; * « md » le mode mineur descendant. {| class="wikitable" |+ Accords de trois notes ! scope="col" rowspan="2" | Nom ! scope="col" rowspan="2" | 3<sup>ce</sup> ! scope="col" rowspan="2" | 5<sup>te</sup> ! scope="col" rowspan="2" | État fondamental ! scope="col" rowspan="2" | 1<sup>er</sup> renversement ! scope="col" rowspan="2" | 2<sup>nd</sup> renversement ! scope="col" colspan="4"| Construit sur les degrés |- ! scope="col" | M ! scope="col" | mh ! scope="col" | ma ! scope="col" | md |- | Accord parfait<br /> majeur || M || J | accord de quinte || accord de sixte || accord de<br />sixte et de quarte | {{Times New Roman|I, IV, V}} || {{Times New Roman|V, VI}} || {{Times New Roman|IV, V}} || {{Times New Roman|III, VI, VII}} |- | Accord parfait<br /> mineur || m || J | accord de quinte || accord de sixte || accord de<br />sixte et de quarte | {{Times New Roman|II, III, VI}} || {{Times New Roman|I, IV}} || {{Times New Roman|I, II}} || {{Times New Roman|I, IV, V}} |- | Accord de<br />quinte diminuée || m || d | accord de<br />quinte diminuée || accord de<br />sixte sensible<br />sans fondamentale || accord de triton<br />sans fondamentale | {{Times New Roman|VII (“V”)}} || {{Times New Roman|II, VII (“V”)}} || {{Times New Roman|VI, VII (“V”)}} || {{Times New Roman|II}} |- | Accord de<br />quinte augmentée || M || A | accord de<br />quinte augmentée || accord de sixte<br />et de tierce sensible || accord de sixte et de quarte<br />sur sensible | &nbsp; || {{Times New Roman|III}} || {{Times New Roman|III}} || &nbsp; |} ==== Accords de quatre notes ==== Les accords de quatre notes sont des accord composés de trois tierces superposées. La dernière note étant le septième degré de la gamme, on parle aussi d'accords de septième. Ces accords sont dissonants : ils contiennent un intervalle de septième (soit une octave montante suivie d'une seconde descendante). Ils laissent donc une impression de « tension ». Il existe sept différents types d'accords, ou « espèces ». Citons l'accord de septième de dominante, l'accord de septième mineure et l'accord de septième majeure. ===== L'accord de septième de dominante ===== [[Fichier:Accord 7e dominante do majeur renversements chiffre.svg|vignette|Accord de septième de dominante de ''do'' majeur et ses renversements, chiffrés.]] L'accord de septième de dominante est l'empilement de trois tierces à partir de la dominante de la gamme, c'est-à-dire du {{Times New Roman|V}}<sup>e</sup> degré. Par exemple, l'accord de septième de dominante de ''do'' majeur est l'accord ''sol''-''si''-''ré''-''fa'', et l'accord de septième de dominante de ''la'' mineur est ''mi''-''sol''♯-''si''-''ré''. L'accord de septième de dominante dont la fondamentale est ''do'' (''do''-''mi''-''sol''-''si''♭) appartient à la gamme de ''fa'' majeur. Que le mode soit majeur ou mineur, il est composé d'une tierce majeure, d'une quinte juste et d'une septième mineure (c'est un accord parfait majeur auquel on ajoute une septième mineure). C'est de loin l'accord de septième le plus utilisé ; il apparaît au {{pc|xvii}}<sup>e</sup> en musique classique. Dans son état fondamental, son chiffrage est {{Times New Roman|V 7/+}} (ou {{Times New Roman|V<sup>7</sup><sub>+</sub>}}). Le signe plus indique la sensible. Son premier renversement est appelé « accord de quinte diminuée et sixte » et est noté {{Times New Roman|V 6/<s>5</s>}} (ou {{Times New Roman|V<sup>6</sup><sub><s>5</s></sub>}}). Son deuxième renversement est appelé « accord de sixte sensible », puisque la sixte de l'accord est la sensible de la gamme, et est noté {{Times New Roman|V +6}} (ou {{Times New Roman|V<sup>+6</sup>}}). Son troisième renversement est appelé « accord de quarte sensible » et est noté {{Times New Roman|V +4}} (ou {{Times New Roman|V<sup>+4</sup>}}). [[Fichier:Accord 7e dominante sans fondamentale do majeur renversements chiffre.svg|vignette|Accord de septième de dominante sans fondamentale de ''do'' majeur et ses renversements, chiffrés.]] On utilise aussi l'accord de septième de dominante sans fondamentale ; c'est alors un accord de trois notes. Dans son état fondamental, c'est un « accord de quinte diminuée » placé sur le {{Times New Roman|VII}}<sup>e</sup> degré (mais c'est bien un accord construit sur le {{Times New Roman|V}}<sup>e</sup> degré), noté {{Times New Roman|“V” <s>5</s>}} (ou {{Times New Roman|“V”<sup><s>5</s></sup>}}). Notez les guillemets qui indiquent que la fondamentale V est absente. Dans son premier renversement, c'est un « accord de sixte sensible sans fondamentale » noté {{Times New Roman|“V” +6/3}} (ou {{Times New Roman|“V”<sup>+6</sup><sub>3</sub>}}). Dans son second renversement, c'est un « accord de triton sans fondamentale » (puisque le premier intervalle est une quarte augmentée qui comporte trois tons) noté {{Times New Roman|“V” 6/+4}} (ou {{Times New Roman|“V”<sup>6</sup><sub>+4</sub>}}). Notons qu'un accord de septième de dominante n'a pas toujours la dominante pour fondamentale : tout accord composé d'une tierce majeure, d'une quinte juste et d'une septième mineure est un accord de septième de dominante et est chiffré {{Times New Roman|<sup>7</sup><sub>+</sub>}}, quel que soit le degré sur lequel il est bâti (certaines notes peuvent avoir une altération accidentelle). ===== Les accords de septième d'espèce ===== Les autres accords de septièmes sont dits « d'espèce ». L'accord de septième mineure est l'accord de septième formé sur la fondamentale d'une gamme mineure ''naturelle''. Par exemple, l'accord de septième mineure de ''la'' est ''la''-''do''-''mi''-''sol''. Il est composé d'une tierce mineure, d'une quinte juste et d'une septième mineure (c'est un accord parfait mineur auquel on ajoute une septième mineure). L'accord de septième majeure est l'accord de septième formé sur la fondamentale d'une gamme majeure. Par exemple, L'accord de septième majeure de ''do'' est ''do''-''mi''-''sol''-''si''. Il est composé d'une tierce majeure, d'une quinte juste et d'une septième majeure (c'est un accord parfait majeur auquel on ajoute une septième majeure). ==== Utilisation du chiffrage ==== Le chiffrage est utilisé de deux manières. La première manière, c'est la notation de la basse continue. La basse continue est une technique d'improvisation utilisée dans le baroque pour l'accompagnement d'instruments solistes. Sur la partition, on indique en général la note de basse de l'accord et le chiffrage en chiffres arabes. La seconde manière, c'est pour l'analyse d'une partition. Le fait de chiffrer les accords permet de mieux en comprendre la structure. De manière générale, on peut retenir que : * le chiffrage « 5 » indique un accord parfait, superposition d'une tierce (majeure ou mineure) et d'une quinte juste ; * le chiffrage « 6 » indique le premier renversement d'un accord parfait ; * le chiffrage « 6/4 » indique le second renversement d'un accord parfait ; * chiffrage « 7/+ » indique un accord de septième de dominante ; * le signe « + » indique en général que la note de l'intervalle est la sensible ; * un intervalle barré désigne un intervalle diminué. [[fichier:Accords gamme do majeur la mineur.svg|class=transparent| center | Principaux accords construits sur les gammes de ''do'' majeur et de ''la'' mineur harmonique.]] === Notation « jazz » === En jazz et de manière générale en musique rock et populaire, la base d'un accord est la triade composée d'une tierce (majeure ou mineure) et d'une quinte juste. Pour désigner un accord, on utilise la note fondamentale, éventuellement désigné par une lettre dans le système anglo-saxon (A pour ''la'' etc.), suivi d'une qualité (comme « m », « + »…). Les renversements ne sont pas notés de manière particulière, ils sont notés comme les formes fondamentales. Dans les deux tableaux suivants, la fondamentale est notée X (remplace le C pour un accord de ''do'', le D pour un accord de ''ré''…). La construction des accords est décrite par la suite. [[Fichier:Arbre accords triades 5d5J5A.svg|vignette|upright=1.5|Formation des triades présentée sous forme d'arbre.]] {| class="wikitable" |+ Notation des principales triades |- | ! scope="col" | Tierce<br />mineure (3m) ! scope="col" | Tierce<br />majeure (3M) |- ! scope="row" | Quinte diminuée (5d) | X<sup>o</sup>, Xm<sup>♭5</sup>, X–<sup>♭5</sup> || |- ! scope="row" | Quinte juste (5J) | Xm, X– || X |- ! scope="row" | Quinte augmentée (5A) | || X+, X<sup>♯5</sup> |} [[Fichier:Triades do.svg|class=transparent|center|Triades de do.]] {| class="wikitable" |+ Notation des principaux accords de septième |- | colspan="2" | ! scope="col" | Tierce<br />mineure (3m) ! scope="col" | Tierce<br />majeure (3M) |- ! scope="row" rowspan="2" | Quinte<br />diminuée (5d) ! scope="row" | Septième diminuée (7d) | X<sup>o7</sup> || |- ! scope="row" | Septième mineure (7m) | Xm<sup>7(♭5)</sup>, X–<sup>7(♭5)</sup>, X<sup>Ø</sup> || |- ! scope="row" rowspan="3" | Quinte<br />juste (5J) ! scope="row" | Sixte majeure (6M) | Xm<sup>6</sup> || X<sup>6</sup> |- ! scope="row" | Septième mineure (7m) | Xm<sup>7</sup>, X–<sup>7</sup> || X<sup>7</sup> |- ! scope="row" | Septième majeure (7M) | Xm<sup>maj7</sup>, X–<sup>maj7</sup>, Xm<sup>Δ</sup>, X–<sup>Δ</sup> || X<sup>maj7</sup>, X<sup>Δ</sup> |- ! scope="row" rowspan="2" | Quinte<br />augmentée (5A) ! scope="row" | Septième mineure (7m) | || X+<sup>7</sup> |- ! scope="row" | Septième majeure (7M) | || X+<sup>maj7</sup> |} [[Fichier:Arbre accords septieme.svg|class=transparent|center|Formation des accords de septième présentée sous forme d'arbre.]] &nbsp; &nbsp; [[Fichier:Accords do septieme.svg|class=transparent|center|Accord de do septième.]] On notera que l'intervalle de sixte majeure est l'enharmonique de celui de septième diminuée (6M = 7d). [[File:Principaux accords do.svg|class=transparent|center|Principaux accords de do.]] ==== Triades ==== ; Accords fondés sur une tierce majeure * accord parfait majeur : pas de notation *: p. ex. « ''do'' » ou « C » pour l'accord parfait de ''do'' majeur (''do'' - ''mi'' - ''sol'') ; Accords fondés sur une tierce mineure * accord parfait mineur : « m », « min » ou « – » *: « ''do'' m », « ''do'' – », « Cm », « C– »… pour l'accord parfait de ''do'' mineur (''do'' - ''mi''♭ - ''sol'') ==== Triades modifiées ==== ; Accords fondés sur une tierce majeure * accord augmenté (la quinte est augmentée) : aug, +, ♯5 *: « ''do'' aug », « ''do'' + », « ''do''<sup>♯5</sup> » « Caug », « C+ » ou « C<sup>♯5</sup> » pour l'accord de ''do'' augmenté (''do'' - ''mi'' - ''sol''♯) : L'accord augmenté est un empilement de tierces majeures. Ainsi, un accord augmenté a deux notes communes avec deux autres accords augmentés : C+ (''do'' - ''mi'' - ''sol''♯) a deux notes communes avec A♭+ (''la''♭ - ''do'' - ''mi'') et avec E+ (''mi'' - ''sol''♯ - ''si''♯) ; et on remarque que ces trois accords sont en fait enharmoniques (avec les enharmonies ''la''♭ = ''sol''♯ et ''si''♯ = ''do''). En effet, l'octave comporte six tons (sous la forme de cinq tons et deux demi-tons), et une tierce majeure comporte deux tons, on arrive donc à l'octave en ajoutant une tierce majeure à la dernière note de l'accord. ; Accords fondés sur une tierce mineure * accord diminué (la quinte est diminuée) : dim, o, ♭5 *: « ''do'' dim », « ''do''<sup>o</sup> », « ''do''<sup>♭5</sup> », « Cdim », « C<sup>o</sup> » ou « C<sup>♭5</sup> » pour l'accord de ''do'' diminuné (''do'' - ''mi''♭ - ''sol''♭) : On remarque que la quinte diminuée est l'enharmonique de la quarte augmentée et est l'intervalle appelé « triton » (car composé de trois tons). ; Accords fondés sur une tierce majeure ou mineure * accord suspendu de seconde : la tierce est remplacée par une seconde majeure : sus2 *: « ''do''<sup>sus2</sup> » ou « C<sup>sus2</sup> » pour l'accord de ''do'' majeur suspendu de seconde (''do''-''ré''-''sol'') * accord suspendu de quarte : la tierce est remplacée par une quarte juste : sus4 *: « ''do''<sup>sus4</sup> » ou « C<sup>sus4</sup> » pour l'accord de ''do'' majeur suspendu de quarte (''do''-''fa''-''sol'') ==== Triades appauvries ==== ; Accords fondés sur une tierce majeure ou mineure * accord de puissance : la tierce est omise, l'accord n'est constitué que de la fondamentale et de la quinte juste : 5 *: « ''do''<sup>5</sup> », « C<sup>5</sup> » pour l'accord de puissance de ''do'' (''do'' - ''la'') {{note|Très utilisé dans les musiques rock, hard rock et heavy metal, il est souvent joué renversé (''la'' - ''do'') ou bien avec l'ajout de l'octave (''do'' - ''la'' - ''do'').}} ==== Triades enrichies ==== ; Accords fondés sur une tierce majeure * accord de septième (la 7<sup>e</sup> est mineure) : 7 *: « ''do''<sup>7</sup> », « C<sup>7</sup> » pour l'accord de ''do'' septième, appelé « accord de septième de dominante de ''fa'' majeur » en musique classique (''do'' - ''mi'' - ''sol'' - ''si''♭) * accord de septième majeure : Δ, 7M ou maj7 *: « ''do'' <sup>Δ</sup> », « ''do'' <sup>maj7</sup> », « C<sup>Δ</sup> », « C<sup>7M</sup> »… pour l'accord de ''do'' septième majeure (''do'' - ''mi'' - ''sol'' - ''si'') ; Accords fondés sur une tierce mineure * accord de mineur septième (la tierce et la 7<sup>e</sup> sont mineures) : m7, min7 ou –7 *: « ''do'' m<sup>7</sup> », « ''do'' –<sup>7</sup> », « Cm<sup>7</sup> », « C–<sup>7</sup> »… pour l'accord de ''do'' mineur septième, appelé « accord de septième de dominante de ''fa'' mineur » en musique classique (''do'' - ''mi''♭ - ''sol'' - ''si''♭) * accord mineure septième majeure : m7M, m7maj, mΔ, –7M, –7maj, –Δ *: « ''do'' m<sup>7M</sup> », « ''do'' m<sup>maj7</sup> », « ''do'' –<sup>Δ</sup> », « Cm<sup>7M</sup> », « Cm<sup>maj7</sup> », « C–<sup>Δ</sup> »… pour l'accord de ''do'' mineur septième majeure (''do'' - ''mi''♭ - ''sol'' - ''si'') * accord de septième diminué (la quinte et la septième sont diminuée) : dim 7 ou o7 *: « ''do'' dim<sup>7</sup> », « ''do''<sup>o7</sup> », « Cdim<sup>7</sup> » ou « C<sup>o7</sup> » pour l'accord de ''do'' septième diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si''♭) * accord demi-diminué (seule la quinte est diminuée, la septième est mineure) : Ø ou –7(♭5) *: « ''do''<sup>Ø</sup> », « ''do''<sup>7(♭5)</sup> », « C<sup>Ø</sup> » ou « C<sup>7♭5</sup> » pour l'accord de ''do'' demi-diminué (''do'' - ''mi''♭ - ''sol''♭ - ''si'') === Construction pythagoricienne des accords === Nous avons vu au débuts que lorsque l'on joue deux notes en même temps, leurs vibrations se superposent. Certaines superpositions créent un phénomène de battement désagréable, c'est le cas des secondes. Dans le cas d'une tierce majeure, les fréquences des notes quadruple et quintuple d'une même base : les fréquences s'écrivent 4׃<sub>0</sub> et 5׃<sub>0</sub>. Cette superposition de vibrations est agréable à l'oreille. Nous avons également vu que dans le cas d'une quinte juste, les fréquences sont le double et le triple d'une même base, ou encore le quadruple et sextuple si l'on considère la moitié de cette base. Ainsi, dans un accord parfait majeur, les fréquences des fondamentales des notes sont dans un rapport 4, 5, 6. De même, dans le cas d'un accord parfait mineur, les proportions sont de 1/6, 1/5 et 1/4. {{voir|[[../Caractéristiques_et_notation_des_sons_musicaux#Construction_pythagoricienne_et_gamme_de_sept_tons|Caractéristiques et notation des sons musicaux &gt; Construction pythagoricienne et gamme de sept tons]]}} === Un peu de physique : interférences === Les sons sont des vibrations. Lorsque l'on émet deux sons ou plus simultanément, les vibrations se superposent, on parle en physique « d'interférences ». Le modèle le plus simple pour décrire une vibration est la [[w:fr:Fonction sinus|fonction sinus]] : la pression de l'air P varie en fonction du temps ''t'' (en secondes, s), et l'on a pour un son « pur » : : P(''t'') ≈ sin(2π⋅ƒ⋅''t'') où ƒ est la fréquence (en hertz, Hz) du son. Si l'on émet deux sons de fréquence respective ƒ<sub>1</sub> et ƒ<sub>2</sub>, alors la pression vaut : : P(''t'') ≈ sin(2π⋅ƒ<sub>1</sub>⋅''t'') + sin(2π⋅ƒ<sub>2</sub>⋅''t''). Nous avons ici une [[w:fr:Identité trigonométrique#Transformation_de_sommes_en_produits,_ou_antilinéarisation|identité trigonométrique]] dite « antilinéarisation » : : <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{f_1 + f_2}{2}t \right ) \cdot \sin \left ( 2\pi \frac{f_1 - f_2}{2}t \right ).</math> On peut étudier simplement deux situations simples. [[Fichier:Battements interferentiels.png|vignette|Deux sons de fréquences proches créent des battements : la superposition d'une fréquence et d'une enveloppe.]] La première, c'est quand les fréquences ƒ<sub>1</sub> et ƒ<sub>2</sub> sont très proches. Alors, la moyenne (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2 est très proche de ƒ<sub>1</sub> et ƒ<sub>2</sub> ; et la demie différence (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2 est très proche de zéro. On a donc une enveloppe de fréquence très faible, (ƒ<sub>1</sub> – ƒ<sub>2</sub>)/2, dans laquelle s'inscrit un son de fréquence moyenne, (ƒ<sub>1</sub> + ƒ<sub>2</sub>)/2. C'est cette enveloppe de fréquence très faible qui crée les battements, désagréables à l'oreille. Sur l'image ci-contre, le premier trait rouge montre un instant où les vibrations sont opposées ; elles s'annulent, le son s'éteint. Le second trait rouge montre un instant où les vibrations sont en phase : elle s'ajoutent, le son est au plus fort. {{clear}} La seconde, c'est lorsque les deux fréquences sont des multiples entiers d'une même fréquence fondamentale ƒ<sub>0</sub> : ƒ<sub>1</sub> = ''n''<sub>1</sub>⋅ƒ<sub>0</sub> et ƒ<sub>0</sub> = ''n''<sub>0</sub>⋅ƒ<sub>0</sub>. On a alors : : <math>\mathrm{P}(t) = 2 \cdot \sin \left ( 2\pi \frac{n_1 + n_2}{2}f_0 \cdot t \right ) \cdot \sin \left ( 2\pi \frac{n_1 - n_2}{2}f_0 \cdot t \right ).</math> On multiplie donc deux fonctions qui ont des fréquences multiples de ƒ<sub>0</sub>. La différence minimale entre ''n''<sub>1</sub> et ''n''<sub>2</sub> vaut 1 ; on a donc une enveloppe dont la fréquence est au minimum la moitié de ƒ<sub>0</sub>, c'est-à-dire un son une octave en dessous de ƒ<sub>0</sub>. Donc, cette enveloppe ne crée pas d'effet de battement, ou plutôt, le battement est trop rapide pour être perçu comme tel. Dans cette enveloppe, on a une fonction sinus dont la fréquence est également un multiple de ƒ<sub>0</sub> ; l'enveloppe et la fonction qui y est inscrite ont donc de nombreux « points communs », d'où l'effet harmonieux. === Le tonnetz === [[File:Speculum musicae.png|thumb|right|225px|Euler, ''De harmoniæ veris principiis'', 1774, p. 350.]] En allemand, le terme ''Tonnetz'' (se prononce « tône-netz ») signifie « réseau tonal ». C'est une représentation graphique des notes qui a été imaginée par [[w:Leonhard Euler|Leonhard Euler]] en 1739. Cette représentation graphique peut aider à la mémorisation de certains concepts de l'harmonie. Cependant, son application est très limitée : elle ne concerne que l'intonation juste d'une part, et que les accords parfait des tonalités majeures et mineures naturelles d'autre part. La représentation contenant les douze notes de la musique savante occidentale, on peut bien sûr représenter d'autres objets, comme les accords de septième ou les accords diminués, mais la représentation graphique est alors compliquée et perd son intérêt pédagogique. On part d'une note, par exemple le ''do''. Si on progresse vers la droite, on monte d'une quinte juste, donc ''sol'' ; vers la gauche, on descend d'une quinte juste, donc ''fa''. Si on va vers le bas, on monte d'une tierce majeure, donc ''mi'' ; si on va vers le haut, on descend d'une tierce majeure, donc ''la''♭ ou ''sol''♯ fa — do — sol — ré | | | | la — mi — si — fa♯ | | | | do♯ — sol♯ — ré♯ — si♭ La figure forme donc un filet, un réseau. On voit que ce réseau « boucle » : si on descend depuis le ''do''♯, on monte d'une tierce majeure, on obtient un ''mi''♯ qui est l'enharmonique du ''fa'' qui est en haut de la colonne. Si on va vers la droite à partir du ''ré'', on obtient le ''la'' qui est au début de la ligne suivante. Si on ajoute des diagonales allant vers la droite et le haut « / », on met en évidence des tierces mineures : ''la'' - ''do'', ''mi'' - ''sol'', ''si'' - ''ré'', ''do''♯ - ''mi''… fa — do — sol — ré | / | / | / | la — mi — si — fa♯ | / | / | / | do♯ — sol♯ — ré♯ — si♭ Donc les liens représentent : * | : tierce majeure ; * — : quinte juste ; * / : tierce mineure. [[Fichier:Tonnetz carre accords fr.svg|thumb|Tonnetz avec les accords parfaits. Les notes sont en notation italienne et les accords en notation jazz.]] On met ainsi en évidence des triangles dont un côté est une quinte juste, un côté une tierce majeure et un côté une tierce mineure ; c'est-à-dire que les notes aux sommets du triangle forment un accord parfait majeur (par exemple ''do'' - ''mi'' - ''sol'') : <div style="font-family:courier; background-color:#fafafa"> fa&nbsp;&nbsp;—&nbsp;'''do&nbsp;&nbsp;&nbsp;—&nbsp;sol'''&nbsp;—&nbsp;ré<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;'''|&nbsp;&nbsp;&nbsp;/'''&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;|<br /> la&nbsp;&nbsp;—&nbsp;'''mi'''&nbsp;&nbsp;&nbsp;—&nbsp;si&nbsp;&nbsp;—&nbsp;fa♯<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;/&nbsp;&nbsp;|<br /> do♯&nbsp;—&nbsp;sol♯&nbsp;—&nbsp;ré♯&nbsp;—&nbsp;si♭ </div> ou un accord parfait mineur (''la'' - ''do'' - ''mi''). <div style="font-family:courier; background-color:#fafafa"> fa&nbsp;&nbsp;—&nbsp;'''do'''&nbsp;&nbsp;&nbsp;—&nbsp;sol&nbsp;—&nbsp;ré<br /> |&nbsp;&nbsp;'''/&nbsp;&nbsp;|'''&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;|<br /> '''la&nbsp;&nbsp;—&nbsp;mi'''&nbsp;&nbsp;&nbsp;—&nbsp;si&nbsp;&nbsp;—&nbsp;fa♯<br /> |&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;|&nbsp;&nbsp;/&nbsp;&nbsp;|<br /> do♯&nbsp;—&nbsp;sol♯&nbsp;—&nbsp;ré♯&nbsp;—&nbsp;si♭ </div> Un triangle représente donc un accord, et un sommet représente une note. Si on passe d'un triangle à un triangle voisin, alors on passe d'un accord à un autre accord, les deux accords ayant deux notes en commun. Ceci illustre la notion de « plus court chemin » en harmonie : si on passe d'un accord à un autre en gardant un côté commun, alors on a un mouvement conjoint sur une seule des trois voix. Par rapport à l'harmonie fonctionnelle : les accords sont contigus à leur fonction, par exemple en ''do'' majeur : * fonction de tonique ({{Times New Roman|I}}) : C, A– et E– sont contigus ; * fonction de sous-dominante ({{Times New Roman|IV}}) : F et D– sont contigus ; * fonction de dominante ({{Times New Roman|V}}) : G et B<sup>o</sup> sont contigus. On notera que les triangles d'un schéma ''tonnetz'' ne représentent que des accords parfaits. Pour représenter un accord de quinte diminuée (''si'' - ''ré'' - ''fa'') ou les accords de septième, en particulier l'accord de septième de dominante, il faut étendre le ''tonnetz'' et l'on obtient des figures différentes. Par ailleurs, il est adapté à ce que l'on appelle « l'intonation juste », puisque tous les intervalles sont idéaux. [[Fichier:Tonnetz carre accords etendu fr.svg|vignette|Tonnetz étendu.]] [[Fichier:Tonnetz carre do majeur accords fr.svg|vignette|Tonnetz de la tonalité de ''do'' majeur. La représentation de l'accord de quinte diminuée sur ''si'' (B<sup>o</sup>) est une ligne et non un triangle.]] [[Fichier:Tonnetz carre do mineur accords fr.svg|vignette|Tonnetz des tonalités de ''do'' mineur naturel (haut) et ''do'' mineur harmonique (bas).]] Si l'on étend un peu le réseau : ré♭ — la♭ — mi♭ — si♭ — fa | / | / | / | / | fa — do — sol — ré — la | / | / | / | / | la — mi — si — fa♯ — do♯ | / | / | / | / | do♯ — sol♯ — ré♯ — la♯ — mi♯ | / | / | / | / | mi♯ — do — sol — ré — la on peut donc trouver des chemins permettant de représenter les accords de septième de dominante (par exemple en ''do'' majeur, G<sup>7</sup>) fa / sol — ré | / si et des accords de quinte diminuée (en ''do'' majeur : B<sup>o</sup>) fa / ré / si Une gamme majeure ou mineure naturelle peut se représenter par un trapèze rectangle : ''do'' majeur fa — do — sol — ré | / la — mi — si et ''do'' mineur la♭ — mi♭ — si♭ / | fa — do — sol — ré En revanche, la représentation d'une tonalité nécessite d'étendre le réseau afin de pouvoir faire figurer tous les accords, deux notes sont représentées deux fois. La représentation des tonalités mineures harmoniques prend une forme biscornue, ce qui nuit à l'intérêt pédagogique de la représentation. [[Fichier:Neo-Riemannian Tonnetz.svg|vignette|upright=2|Tonnetz avec des triangles équilatéraux.]] On peut réorganiser le schéma en décalant les lignes, afin d'avoir des triangles équilatéraux. Sur la figure ci-contre (en notation anglo-saxonne) : * si on monte en allant vers la droite « / », on a une tierce mineure ; * si on descend en allant vers la droite « \ », on a une tierce majeure ; * les liens horizontaux « — » représentent toujours des quintes justes * les triangles pointe en haut sont des accords parfaits mineurs ; * les triangles pointe en bas sont des accords parfaits majeurs. On a alors les accords de septième de dominante F / G — D \ / B et de quinte diminuée F / D / B les tonalités majeures F — C — G — D \ / A — E — B et les tonalités mineures naturelles A♭ — E♭ — B♭ / \ F — C — G — D == Notes et références == {{références}} == Voir aussi == === Bibliographie === * {{ouvrage |prénom1=Francesco |nom1=Gasparini |titre=L'Armonico Pratico al Cimbalo |éditeur=Giusseppe Antonio Silvani |lieu=Bologne |année=1772 |lang=it |url=https://imslp.org/wiki/L'Armonico_Pratico_al_Cimbalo_(Gasparini%2C_Francesco)}} === Liens externes === {{wikipédia|Consonance (harmonie tonale)}} {{wikipédia|Disposition de l'accord}} {{wikisource|Petit Manuel d’harmonie}} * {{lien web | url = https://www.apprendrelesolfege.com/chiffrage-d-accords | titre = Chiffrage d'accords (classique) | site = Apprendrelesolfege.com | consulté le = 2020-12-03 }} * {{lien web | url = https://www.coursd-harmonie.fr/introduction/introduction2_le_chiffrage_d_accords.php | titre = Introduction II : Le chiffrage d'accords | site = Cours d'harmonie.fr | consulté le = 2021-12-14 }} * {{lien web | url=https://www.coursd-harmonie.fr/ | titre = Cours d'harmonie en ligne | auteur = Jean-Baptiste Voinet | site=coursd-harmonie.fr | consulté le = 2021-12-20 }} * {{lien web | url=http://e-harmonie.e-monsite.com/ | titre = Cours d'harmonie classique en ligne | auteur = Olivier Miquel | site=e-harmonie | consulté le = 2021-12-24 }} * {{lien web | url=https://fr.audiofanzine.com/theorie-musicale/editorial/dossiers/les-gammes-et-les-modes.html | titre = Les bases de l’harmonie | site = AudioFanzine | date = 2013-07-23 | consulté le = 2024-01-12 }} * {{lien web | url=https://www.radiofrance.fr/francemusique/podcasts/maxxi-classique/l-accord-de-tristan-dechiffre-et-desacralise-2481605 | titre = L’Accord de Tristan déchiffré et désacralisé ! | site = France Musique | date = 2025-10-22 | consulté le = 2025-10-23 }} * {{lien web | url=https://www.youtube.com/watch?v=X62SAnyfQYw | titre = Qu'est-ce que la basse continue dans la musique baroque ? | auteur = Clément Rochefort | site = YouTube/France Musique | date = 2022-05-02 | consulté le = 2026-08-04 }} ---- ''[[../Mélodie|Mélodie]]'' &lt; [[../|↑]] &gt; ''[[../Représentation musicale|Représentation musicale]]'' [[Catégorie:Formation musicale (livre)|Harmonie]] qztdb5v5tonzcm6d3gfbvn8it0hpi2p Harmonie 0 77770 773902 687589 2026-10-02T07:53:15Z Cdang 1202 organisation 773902 wikitext text/x-wiki {{homonymie}} ; Bonne entente entre plusieurs personnes * [[Grec ancien/Dieux/Harmonie]] ; En musique * sons sonnant simultanément : ** [[Formation musicale/Harmonie]] * harmonique, son ayant une fréquence multiple de la fondamentale : ** [[Apprendre la guitare/Harmoniques]] ; Par extension, fonction mathématique décomposant un signal * [[Recueil d'exercices de mécanique élémentaire/Polynômes harmoniques sphériques]] 0iobez48rdwiv2d9txlm157mwvawsp2 Fonctionnement d'un ordinateur/Processeurs à émissions multiples 0 79442 773900 693004 2026-10-02T04:03:11Z JackBot 14683 Robot : Correction d’une double redirection depuis [[Fonctionnement d'un ordinateur/Les processeurs superscalaires]] vers [[Fonctionnement d'un ordinateur/Les processeurs superscalaires in-order]] 773900 wikitext text/x-wiki #REDIRECTION [[Fonctionnement d'un ordinateur/Les processeurs superscalaires in-order]] 2ikmycsca44t6k7xdwa5oo6re9bg0el Mathc complexes/a277 0 81080 773898 773843 2026-10-01T20:19:13Z Xhungab 23827 773898 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/Sommaire#Utilitaires Matrices| Sommaire]] '''L'étude de ce chapitre peut ce faire à l'aide de cette [[https://youtube.com/playlist?list=PLi6peGpf8EPNHUnib_ehtvFhNxJMWqn5Y&si=KIbSRJw_xjHl815R Playlist]].''' {{Partie{{{type|}}}| Matrices pour la transformée de Fourier discrète (TFD):}} La transformée de Fourier discrète d'un signal numérique s de N échantillons (s(0), s(1),..., s(N-1) est un vecteur (S(0),S(1), ..., S(N-1)) défini par : _ N-1 \ S(k) = | s(n) exp(-2ikPI n/N) pour 0 <= k < N /_n=0 On obtient ainsi une représentation spectrale discrète du signal échantillonné s(n). Notre étude  nous a permis de comprendre que l'on pouvait utiliser le calcul matriciel pour effectuer ce travail. * Dans un premier temps nous construirons manuellement cette matrice. * Dans un deuxième temps, nous demanderons au langage C de construire cette matrice. Nous verrons qu'en prenant le conjugué de cette matrice, multiplié par l'inverse de N, (le nombre d'échantillons), nous allons obtenir '''la transformée de Fourier discrète inverse'''. ==Copier ces fichier avec la bibliothèque sur les matrices complexes :== *[[Mathc complexes/a278| dft.h]] *[[Mathc complexes/a279| f.h]] ==Le fichier principal :== *[[Mathc complexes/a280| c03a.c]] Une matrice 3x3 *[[Mathc complexes/a281| c04a.c]] Une matrice 4x4 *[[Mathc complexes/09y| c04b.c]] Une matrice 4x4 ... [https://fr.wikipedia.org/wiki/Transformation_de_Fourier_discr%C3%A8te#Exemple_d'application_à_un_signal Wikipédia] ... [[Mathc complexes/0a2| La TFD sans les matrices]] ... [[Mathc complexes/0a4| exp(-i 3/2 PI)]] *[[Mathc complexes/0a0| c04c.c]] Une matrice 4x4 ... [https://en.wikipedia.org/wiki/Discrete_Fourier_transform#Example Wikipédia] ... [[Mathc complexes/0a3| La TFD sans les matrices]] *[[Mathc complexes/a282| c0na.c]] Une matrice nxn *[[Mathc complexes/09z| c0nb.c]] Une matrice nxn ... [https://fr.wikipedia.org/wiki/Transformation_de_Fourier_discr%C3%A8te#Exemple_d'application_à_un_signal Wikipédia] ... [[Mathc complexes/0a2| La TFD sans les matrices]] *[[Mathc complexes/0a1| c0nc.c]] Une matrice nxn ... [https://en.wikipedia.org/wiki/Discrete_Fourier_transform#Example Wikipédia] ... [[Mathc complexes/0a3| La TFD sans les matrices]] ==Propriétés :== *[[Mathc complexes/a283| c0bna.c]] La formule de Bessel (Une matrice nxn) {{AutoCat}} q4t6e2xxikc9w9t4g6mxzn2lt6z0nfe 773914 773898 2026-10-02T10:54:32Z Xhungab 23827 773914 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/Sommaire#Utilitaires Matrices| Sommaire]] '''L'étude de ce chapitre peut ce faire à l'aide de cette [[https://youtube.com/playlist?list=PLi6peGpf8EPNHUnib_ehtvFhNxJMWqn5Y&si=KIbSRJw_xjHl815R Playlist]].''' {{Partie{{{type|}}}| Matrices pour la transformée de Fourier discrète (TFD):}} La transformée de Fourier discrète d'un signal numérique s de N échantillons (s(0), s(1),..., s(N-1) est un vecteur (S(0),S(1), ..., S(N-1)) défini par : _ N-1 \ S(k) = | s(n) exp(-2ikPI n/N) pour 0 <= k < N /_n=0 On obtient ainsi une représentation spectrale discrète du signal échantillonné s(n). Notre étude  nous a permis de comprendre que l'on pouvait utiliser le calcul matriciel pour effectuer ce travail. * Dans un premier temps nous construirons manuellement cette matrice. * Dans un deuxième temps, nous demanderons au langage C de construire cette matrice. Nous verrons qu'en prenant le conjugué de cette matrice, multiplié par l'inverse de N, (le nombre d'échantillons), nous allons obtenir '''la transformée de Fourier discrète inverse'''. ==Copier ces fichier avec la bibliothèque sur les matrices complexes :== *[[Mathc complexes/a278| dft.h]] *[[Mathc complexes/a279| f.h]] ==Les fichiers principaux :== *[[Mathc complexes/a280| c03a.c]] Une matrice 3x3 *[[Mathc complexes/a281| c04a.c]] Une matrice 4x4 *[[Mathc complexes/09y| c04b.c]] Une matrice 4x4 ... [https://fr.wikipedia.org/wiki/Transformation_de_Fourier_discr%C3%A8te#Exemple_d'application_à_un_signal Wikipédia] ... [[Mathc complexes/0a2| La TFD sans les matrices]] ... [[Mathc complexes/0a4| exp(-i 3/2 PI)]] *[[Mathc complexes/0a0| c04c.c]] Une matrice 4x4 ... [https://en.wikipedia.org/wiki/Discrete_Fourier_transform#Example Wikipédia] ... [[Mathc complexes/0a3| La TFD sans les matrices]] *[[Mathc complexes/a282| c0na.c]] Une matrice nxn *[[Mathc complexes/09z| c0nb.c]] Une matrice nxn ... [https://fr.wikipedia.org/wiki/Transformation_de_Fourier_discr%C3%A8te#Exemple_d'application_à_un_signal Wikipédia] ... [[Mathc complexes/0a2| La TFD sans les matrices]] *[[Mathc complexes/0a1| c0nc.c]] Une matrice nxn ... [https://en.wikipedia.org/wiki/Discrete_Fourier_transform#Example Wikipédia] ... [[Mathc complexes/0a3| La TFD sans les matrices]] ==Propriétés :== *[[Mathc complexes/a283| c0bna.c]] La formule de Bessel (Une matrice nxn) ==Complément :== Calculer la mesure principale de : *[[Mathc complexes/0a5| (-67 PI)/5]] {{AutoCat}} sqqn2qozupqpxczr75o46d3xhkrxt0w Discussion Wikilivres:Le Bistro/2026 5 83406 773850 773437 2026-10-01T12:42:36Z MediaWiki message delivery 36013 /* Wikidata Sitelink changes in Recent Changes and Watchlist */ nouvelle section 773850 wikitext text/x-wiki == Actualités techniques n° 2026-03 == <section begin="technews-2026-W03"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/03|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * La Fondation Wikimedia a publié des questions directrices pour son plan annuel de juillet 2026 à juin 2027 sur les plateformes [[m:Special:MyLanguage/Wikimedia Foundation Annual Plan/2026-2027/Product & Technology OKRs|Meta]] et ''[[diffblog:2025/12/10/shaping-wikimedia-foundations-2026-2027-annual-goals-key-questions-for-the-wikimedia-movement/|Diff]]''. Celles-ci portent sur les tendances mondiales, une expérimentation plus rapide et plus constructive, un meilleur accompagnement des nouveaux contributeurs, le renforcement du rôle des éditeurs et des utilisateurs avancés, l'amélioration de la collaboration entre les projets, ainsi que le développement et la fidélisation du lectorat. Des commentaires et suggestions sont les bienvenus sur la [[m:Talk:Wikimedia Foundation Annual Plan/2026-2027|page de discussion]]. '''Actualités pour la contribution''' * Dans le cadre des travaux en cours de l'équipe technique communautaire sur le projet [[m:Special:MyLanguage/Community Wishlist/W372|Listes de surveillance multiples]], l'affichage de [[Special:EditWatchlist|Modifier la liste de surveillance]] sera mis à jour entant que qu'une première étape vers la prise en charge de plusieurs listes de surveillance. De plus, la pagination de [[Special:Search|Recherche]] sera également mise à jour, dans le cadre du travail sur le souhait [[m:Special:MyLanguage/Community Wishlist/W186|Refonte de la pagination / navigation des pages]]. [https://phabricator.wikimedia.org/T411596] * [[m:Special:GlobalWatchlist|La Liste de Surveillance Globale]] est une [[mw:Special:MyLanguage/Extension:GlobalWatchlist|extension]] de MediaWiki qui vous permet de voir vos listes de surveillance provenant de différents wikis sur la même page. Il a récemment été mis à jour pour ressembler davantage à la [[Special:Watchlist|Liste de surveillance]] régulière, par exemple en le préparant pour les comptes temporaires dans le masquage IP (y compris le réacheminement des liens des utilisateurs vers les pages de contributions), en mettant les titres de page en gras et en ouvrant les liens dans les résumés d'édition et les balises dans de nouveaux onglets du navigateur. [https://phabricator.wikimedia.org/T398361][https://phabricator.wikimedia.org/T298919][https://phabricator.wikimedia.org/T273526][https://phabricator.wikimedia.org/T286309] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:28|la tâche soumise|les {{formatnum:28}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:28||s}} la semaine dernière]]. Par exemple, le problème selon lequel les blocs globaux ne disposaient pas de l'option permettant de désactiver l'envoi d'e-mails a maintenant été résolu et sera disponible à l'utilisation à partir de la semaine du 13 janvier. [https://phabricator.wikimedia.org/T401293] '''Actualités pour la contribution technique''' * L'[[mw:Special:MyLanguage/VisualEditor/Citation tool|outil de citation VisualEditor]] et les [[mw:Special:MyLanguage/Help:Reference Previews|Aperçus de référence]] prennent désormais en charge "carte" comme type de référence. [https://phabricator.wikimedia.org/T411083] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.10|MediaWiki]]/[[mw:MediaWiki 1.46/wmf.11|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/03|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W03"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 12 janvier 2026 à 20:33 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=29907192 --> == Thank You for Last Year – Join Wiki Loves Ramadan 2026 == Dear Wikimedia communities, We hope you are doing well, and we wish you a happy New Year. ''Last year, we captured light. This year, we’ll capture legacy.'' In 2025, communities around the world shared the glow of Ramadan nights and the warmth of collective iftars. In 2026, ''Wiki Loves Ramadan'' is expanding, bringing more stories, more cultures, and deeper global connections across Wikimedia projects. We invite you to explore the ''Wiki Loves Ramadan 2026'' [[m:Special:MyLanguage/Wiki Loves Ramadan 2026|Meta page]] to learn how you can participate and [[m:Special:MyLanguage/Wiki Loves Ramadan 2026/Participating communities|sign up]] your community. 📷 ''Photo campaign on '' [[c:Special:MyLanguage/Commons:Wiki Loves Ramadan 2026|Wikimedia Commons]] If you have questions about the project, please refer to the FAQs: * [[m:Special:MyLanguage/Wiki Loves Ramadan/FAQ/|Meta-Wiki]] * [[c:Special:MyLanguage/Commons:Wiki Loves Ramadan/FAQ|Wikimedia Commons]] ''Early registration for updates is now open via the '''[[m:Special:RegisterForEvent/2710|Event page]]''''' ''Stay connected and receive updates:'' * [https://t.me/WikiLovesRamadan Telegram channel] * [https://lists.wikimedia.org/postorius/lists/wikilovesramadan.lists.wikimedia.org/ Mailing list] We look forward to collaborating with you and your community. '''The Wiki Loves Ramadan 2026 Organizing Team''' 16 janvier 2026 à 20:44 (CET) <!-- Message envoyé par User:ZI Jony@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Distribution_list/Non-Technical_Village_Pumps_distribution_list&oldid=29879549 --> == <span lang="en" dir="ltr">Tech News: 2026-04</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W04"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/04|Translations]] are available. '''Updates for editors''' * The tray shown on [[Special:Diff|Special:Diff]] in mobile view has been redesigned. It is now collapsed by default, and incorporates a link to undo the edit being viewed, making it easier for mobile editors and reviewers to take action while keeping the interface uncluttered. [https://phabricator.wikimedia.org/T402297] * [[m:Special:GlobalWatchlist|The Global Watchlist]] lets you view your watchlists from multiple wikis on one page. The [[mw:Special:MyLanguage/Extension:GlobalWatchlist|extension]] continues to improve — it now automatically determines the text direction (ensuring correct display of sites with unusual domain names) and shows detailed descriptions for log actions. Later this week, a new permanent link for page creations and CSS classes for each entry element will be added. [https://phabricator.wikimedia.org/T412505][https://phabricator.wikimedia.org/T287929][https://phabricator.wikimedia.org/T262768][https://phabricator.wikimedia.org/T414135] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:32}} community-submitted {{PLURAL:32|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, the previously observed issue in Vector 2022, where anchor link targets were obscured by the sticky header, has now been addressed. [https://phabricator.wikimedia.org/T406114] '''Updates for technical contributors''' * As mentioned in the [[m:Special:MyLanguage/Tech/News/2025/44|October 2025 deprecation announcement]], MediaWiki Interfaces team will begin sunsetting all transform endpoints containing a trailing slash from the MediaWiki REST API the week of January 26. Changes are expected to roll out to all wikis on or before January 30th. All API users currently calling them are encouraged to transition to the non-trailing slash versions. Both endpoint variations can be found, compared, and tested using the [https://test.wikipedia.org/wiki/Special:RestSandbox REST Sandbox]. If you have questions or encounter any problems, please file a ticket in Phabricator to the [https://phabricator.wikimedia.org/project/view/6931/ #MW-Interfaces-Team board]. * Interactive reference documentation for the [[mw:Special:MyLanguage/Wikimedia REST API|Wikimedia REST API]] has moved. Requests to API docs previously hosted through [[mw:Special:MyLanguage/RESTBase|RESTBase]] (e.g.: <code dir=ltr>https://en.wikipedia.org/api/rest_v1/</code>) are now redirected to the [[w:en:Special:RestSandbox|REST Sandbox]]. * The [[mw:Special:MyLanguage/Wikidata Platform|WMF Wikidata Platform team]] (WDP) has published its [[d:Special:MyLanguage/Wikidata:Wikidata Platform team/Newsletter|January 2026 newsletter]]. It includes updates on the legacy full-graph endpoint decommissioning, the User-Agent policy change, the monthly Blazegraph migration office hours, and efforts to reduce regressions caused by the legacy endpoint shutdown. As a reminder, you can [[m:Special:MyLanguage/Global message delivery/Targets/WDP team updates|subscribe to the WDP newsletter]]! * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.46/wmf.12|MediaWiki]] '''Meetings and events''' * The [[mw:Wikimedia Hackathon Northwestern Europe 2026|Wikimedia Hackathon Northwestern Europe 2026]] will take place on 13-14 March 2026 in Arnhem, the Netherlands. Applications opened mid-December and will close soon or when capacity is reached. It's a two-day, technically oriented hackathon bringing together Wikimedians from the region. Hope to see you there! '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/04|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W04"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 19 janvier 2026 à 21:29 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=29943403 --> == Révision annuelle du code universel de conduite et des lignes directrices de l'application == <section begin="announcement-content" /> Nous vous informons que la période de relecture annuelle du Code de conduite universel et des règles d'applications est actuellement ouverte. Vous pouvez faire vos commentaires sur les modifications que vous souhaitez apporter jusqu'au 9 février 2026. C'est la première d'une série d'étapes nécessaires pour la révision annuelle. Vous trouverez [[m:Special:MyLanguage/Universal Code of Conduct/Annual review/2026|d'autres informations et les discussions auxquelles participer sur la page UCoC de Meta]]. Le [[m:Special:MyLanguage/Universal Code of Conduct/Coordinating Committee|Comité de coordination du code universel de conduite]] (U4C &mdash; Universal Code of Conduct Coordinating Committee) est un groupe global dont le rôle est de fournir une implémentation équitable et cohérente de l'UCoC. Cette relecture annuelle a été envisagée et mise en place par l'U4C. Pour plus d'informations et les responsabilités de l'U4C, veuillez lire la [[m:Special:MyLanguage/Universal Code of Conduct/Coordinating Committee/Charter|Charte de l'U4C]]. Veuillez partager ces informations avec les autres membres concernés de votre communauté. -- En coopération avec l'U4C, [[m:User:Keegan (WMF)|Keegan (WMF)]] ([[m:User talk:Keegan (WMF)|discussion]])<section end="announcement-content" /> 19 janvier 2026 à 22:01 (CET) <!-- Message envoyé par User:Keegan (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Distribution_list/Global_message_delivery&oldid=29905753 --> == Actualités techniques n° 2026-05 == <section begin="technews-2026-W05"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/05|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * La Fondation Wikimedia invite à donner des commentaires sur [[m:Special:MyLanguage/Product and Technology Advisory Council/Year1 Reflections and Proposed Way Forward 2026 Update|l’avenir proposé]] du [[:m:Special:MyLanguage/Product and Technology Advisory Council|Conseil consultatif des produits et technologies]] jusqu’au 28 février. * Tous les utilisateurs disposant d'un compte enregistré peuvent désormais utiliser des clés d'accès pour la [[m:Special:MyLanguage/Help:Two-factor authentication|double authentification]] (2FA). Les clés d'accès sont un moyen simple de se connecter sans utiliser un second appareil. Elles vérifient l'identité de l'utilisateur à l'aide d'une empreinte digitale, d'une reconnaissance faciale ou d'un code PIN. Pour configurer une clé d'accès, configurez d'abord une méthode 2FA classique. Actuellement, pour se connecter avec une clé d'accès, les utilisateurs doivent également utiliser un mot de passe. Plus tard ce trimestre, la connexion sans mot de passe permettra aux utilisateurs de se connecter d'un simple clic avec une clé d'accès. Les utilisateurs disposant de droits avancés devront également avoir la 2FA activée. Cela fait partie du projet [[mw:Special:MyLanguage/Product Safety and Integrity/Account Security|Sécurité du compte]]. * Les contributeurs non enregistrés sur des IP bloquées ou des plages d'IP bloquées peuvent désormais interagir sur le wiki pour faire appel d'un blocage en créant un compte temporaire afin de contester un blocage sur la page de discussion de l'utilisateur, sauf si l'option « empêcher cet utilisateur de modifier sa propre page de discussion » est activée. Cela résout le problème des utilisateurs déconnectés incapables d'utiliser le processus de déblocage par défaut via la page de discussion de l'utilisateur. [https://phabricator.wikimedia.org/T398673] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:20|la tâche soumise|les {{formatnum:20}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:20||s}} la semaine dernière]]. Par exemple, la description des méthodes d'authentification à deux facteurs (2FA) sur la page de gestion a été mise à jour. Il est désormais plus clair et plus facile pour les utilisateurs à comprendre et à utiliser. [https://phabricator.wikimedia.org/T332385] '''Actualités pour la contribution technique''' * Une nouvelle variable AbuseFilter, <code>account_type</code>, a été ajoutée pour fournir un moyen fiable de déterminer le type de compte créé dans les actions <code>createaccount</code> et <code>autocreateaccount</code>. Dans le cadre de ce changement, la variable <code>accountname</code> a été renommée en <code>account_name</code>, et <code>accountname</code> est désormais obsolète. Les gestionnaires de filtres doivent mettre à jour tous les filtres qui utilisent des vérifications de type de compte codées en dur ou la variable obsolète. [https://phabricator.wikimedia.org/T414049] * Les vignettes d'images demandées dans des tailles non standard, et en utilisant des méthodes non standard telles que les requêtes directes à <code dir=ltr><nowiki>upload.wikimedia.org/…</nowiki></code>, cesseront de fonctionner dans un proche avenir. Ce changement vise à prévenir les abus externes continus par des robots et des aspirateurs web. Certains utilisateurs ayant des CSS/JS personnalisés, les administrateurs d'interface qui peuvent corriger les gadgets et les thèmes locaux, ainsi que les auteurs d'outils, devront mettre à jour leur code pour utiliser des tailles de vignettes standard. [[phab:T414805|Des détails, des liens de recherche et des exemples de correction sont disponibles dans la tâche]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.13|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/05|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W05"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 26 janvier 2026 à 22:17 (CET) <!-- Message envoyé par User:UOzurumba (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=29969530 --> == <span lang="en" dir="ltr">Tech News: 2026-06</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W06"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/06|Translations]] are available. '''Updates for editors''' * The "{{int:pageinfo-toolboxlink}}" feature, which gives validating information about a page ([{{fullurl:{{FULLPAGENAME}}|action=info}} example]), now automatically includes a table of contents. If there is a local [[{{ns:8}}:Pageinfo-header]] page created by individual users, it can now be removed. [https://phabricator.wikimedia.org/T363726] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:21}} community-submitted {{PLURAL:21|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, VisualEditor previously added bold or italic formatting inside link descriptions, making the wikicode complex. This has now been fixed. [https://phabricator.wikimedia.org/T409669] '''Updates for technical contributors''' * There was no XML dump on 20 January. Additionally, from now on, dumps will be generated once per month only. [https://phabricator.wikimedia.org/T414389] * The MediaWiki Interfaces team removed support for all transform endpoints containing a trailing slash from the [https://www.mediawiki.org/wiki/Special:MyLanguage/API:REST%20API MediaWiki REST API]. All API users currently calling those endpoints are encouraged to transition to the non-trailing slash versions. If you have questions or encounter any problems, please file a ticket in phabricator to the [https://phabricator.wikimedia.org/project/view/6931/ #MW-Interfaces-Team board]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.46/wmf.14|MediaWiki]] '''Weekly highlight''' * Users are reminded that the Wikimedia Foundation has shared some guiding questions for the July 2026–June 2027 Annual Plan on [[m:Special:MyLanguage/Wikimedia Foundation Annual Plan/2026-2027/Product & Technology OKRs|Meta]] and ''[[diffblog:2025/12/10/shaping-wikimedia-foundations-2026-2027-annual-goals-key-questions-for-the-wikimedia-movement/|Diff]]''. These focus on global trends, faster and healthier experimentation, better support for newcomers, strengthening editors and advanced users, improving collaboration across projects, and growing and retaining readership. Feedback and ideas are welcome on the [[m:Talk:Wikimedia Foundation Annual Plan/2026-2027|talk page]]. '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/06|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W06"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 2 février 2026 à 18:43 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30000986 --> == Actualités techniques n° 2026-07 == <section begin="technews-2026-W07"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/07|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * [[File:Maki-gift-15.svg|12px|link=|class=skin-invert|Concerne un souhait]] Les contributeurs connectés qui gèrent de grandes ou complexes listes de suivi peuvent désormais organiser et filtrer les pages surveillées de manière à améliorer leurs flux de travail grâce à la nouvelle fonctionnalité [[mw:Special:MyLanguage/Help:Watchlist labels|Étiquettes de liste de suivi]]. En ajoutant des étiquettes personnalisées (par exemple : pages que vous avez créées, pages surveillées pour vandalisme, ou pages de discussion), les utilisateurs peuvent identifier plus rapidement ce qui nécessite une attention, réduire la charge cognitive et répondre plus efficacement. Cela améliore l'utilisabilité de la liste de suivi, en particulier pour les éditeurs très actifs. * Une nouvelle fonctionnalité disponible sur [[Special:Contributions|Special:Contributions]] montre [[mw:Special:MyLanguage/Trust and Safety Product/Temporary Accounts|des comptes temporaires]] qui sont probablement utilisés par la même personne, et rend ainsi le patrouillage moins chronophage. En vérifiant les contributions d'un compte temporaire, les utilisateurs ayant accès aux adresses IP des comptes temporaires peuvent désormais avoir une vue des contributions des comptes temporaires associés. La fonctionnalité recherche toutes les adresses IP associées à un compte temporaire donné pendant la période de conservation des données et affiche toutes les contributions de tous les comptes temporaires ayant utilisé ces adresses IP. [[mw:Special:MyLanguage/Trust and Safety Product/Temporary Accounts#February 2026: Improvements to the patroller tooling|Plus...]] [https://phabricator.wikimedia.org/T415674] * Lorsque les éditeurs prévisualisent une modification de wikitexte, la boîte de rappel indiquant qu'ils ne voient qu'une prévisualisation (qui est affichée en haut) a désormais un fond gris/neutre au lieu d'un fond jaune/d'avertissement. Cela facilite la distinction entre les notes de prévisualisation et les avertissements réels (par exemple, les conflits de modification ou les cibles de redirection problématiques), qui seront désormais affichés dans des boîtes d'avertissement ou d'erreur séparées. [https://phabricator.wikimedia.org/T414742] * La [[m:Special:GlobalWatchlist|Liste de suivi globale]] vous permet de consulter vos listes de suivi provenant de plusieurs wikis sur une seule page. L' [[mw:Special:MyLanguage/Extension:GlobalWatchlist|extension]] continue de s'améliorer — elle prend désormais en charge correctement plus d'un site Wikibase, par exemple à la fois [[d:|Wikidata]] et [[testwikidata:|testwikidata]]. De plus, des problèmes concernant la direction du texte ont été résolus pour les utilisateurs qui préfèrent Wikidata ou d'autres sites Wikibase dans des langues de droite à gauche (RTL). [https://phabricator.wikimedia.org/T415440][https://phabricator.wikimedia.org/T415458] * <span lang="en" dir="ltr" class="mw-content-ltr">The automatic "magic links" for ISBN, RFC, and PMID numbers have been [[mw:Special:MyLanguage/Help:Magic links|deprecated in wikitext since 2021]] due to inflexibility and difficulties with localization. Several wikis have successfully replaced RFC and PMID magic links with equivalent external links, but a template was often required to replace the functionality of the ISBN magic link. There is now a new [[mw:Special:MyLanguage/Help:Magic words#isbn|built-in parser function]] <code dir=ltr><nowiki>{{#isbn}}</nowiki></code> available to replace the basic functionality of the ISBN magic link. This makes it easier for wikis who wish to migrate off of the deprecated magic link functionality to do so.</span> [https://phabricator.wikimedia.org/T145604] * Deux nouveaux wikis ont été créés : ** un {{int:project-localized-name-group-wikipedia}} dans [[d:Q35401|Jju]] ([[w:kaj:|<code>w:kaj:</code>]]) [https://phabricator.wikimedia.org/T413283] ** un {{int:project-localized-name-group-wikipedia}} dans [[d:Q1186896|Nawat]] ([[w:ppl:|<code>w:ppl:</code>]]) [https://phabricator.wikimedia.org/T413273] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:23|la tâche soumise|les {{formatnum:23}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:23||s}} la semaine dernière]]. '''Actualités pour la contribution technique''' * Un nouveau groupe d'utilisateurs global a été créé : [[{{int:grouppage-local-bot}}|{{int:group-local-bot}}]]. Il sera utilisé en interne par le logiciel pour permettre aux robots communautaires de contourner les limites de débit appliquées aux [[w:en:Web_scraping|web scrapers]] abusifs. Les comptes approuvés en tant que robots sur au moins un wiki Wikimedia seront automatiquement ajoutés à ce groupe. Cela ne changera pas les autorisations dont dispose le robot. [https://phabricator.wikimedia.org/T415588] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.15|MediaWiki]] '''Rencontres et évènements''' * La [[mw:Special:MyLanguage/MediaWiki Users and Developers Conference Spring 2026|Conférence des utilisateurs et des développeurs de MediaWiki, Printemps 2026]] se tiendra du 25 au 27 mars à Salt Lake City, États-Unis. Cet événement est organisé par et pour la communauté MediaWiki de tiers. Vous pouvez proposer des sessions et vous inscrire pour y assister. [https://lists.wikimedia.org/hyperkitty/list/wikitech-l@lists.wikimedia.org/thread/AZBWVI46SDEB65PGR5J6E4TYOQQEZXM7/] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/07|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W07"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 10 février 2026 à 00:30 (CET) <!-- Message envoyé par User:Quiddity (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30026671 --> == Actualités techniques n° 2026-08 == <section begin="technews-2026-W08"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/08|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * <span class="mw-translate-fuzzy">L'[[mw:Special:MyLanguage/Wikimedia Site Reliability Engineering|équipe SRE]] va procéder au nettoyage d'[[m:Special:MyLanguage/Etherpad|Etherpad]], l'éditeur web open source de documents collaboratifs en temps réel. Tous les blocs-notes seront définitivement supprimés après le 30 avril 2026 – si des projets de migration sont encore en cours à cette date, l'équipe pourra réexaminer la date au cas par cas. Veuillez effectuer des sauvegardes locales de tout contenu que vous souhaitez conserver, car les données supprimées ne pourront pas être récupérées. Ce nettoyage permet de réduire la taille de la base de données et l'empreinte de l'infrastructure. Etherpad continuera de prendre en charge la collaboration en temps réel, mais le stockage à long terme n'est plus assuré. D'autres nettoyages pourront avoir lieu ultérieurement sans préavis.</span> [https://phabricator.wikimedia.org/T415237] '''Actualités pour la contribution''' * L'équipe de Recherche d'Informations lancera une [[mw:Special:MyLanguage/Readers/Information Retrieval/Phase 1|expérimentation sur l'application mobile Android]], afin de tester des fonctionnalités de recherche hybrides capables de gérer à la fois les requêtes sémantiques et par mots-clés. L'amélioration de la recherche sur la plateforme permettra aux lecteurs de trouver plus facilement ce qu'ils cherchent, directement sur Wikipédia. L'expérimentation sera d'abord lancée sur Wikipédia en grec fin février, puis sur les versions anglaise, française et portugaise en mars. [https://diff.wikimedia.org/2026/01/08/semantic-search-making-it-easier-to-find-the-information-readers-want/ En savoir plus] sur le blog ''Diff''. [https://www.mediawiki.org/wiki/Readers/Information_Retrieval] * L'équipe « Croissance des lecteurs » mènera [[mw:Special:MyLanguage/Readers/Reader Growth/WE3.10.2 Mobile Table of Contents|une expérience]] auprès des utilisateurs de la version mobile du site web qui ajoute une table des matières et développe automatiquement toutes les sections des articles, afin de mieux comprendre les problèmes de navigation qu'ils rencontrent. Le test sera disponible sur les versions arabe, chinoise, anglaise, française, indonésienne et vietnamienne de Wikipedia. * Auparavant, les notifications ([[{{ns:8}}:Sitenotice]] et [[{{ns:8}}:Anonnotice]]) du site ne s'affichaient que sur la version ordinateur. Maintenant, elles s'afficheront désormais sur toutes les plateformes. Les utilisateurs mobiles verront ces notifications. Les administrateurs du site doivent être prêts à tester et à corriger les notifications sur les appareils mobiles afin d'éviter toute interférence avec les articles. Pour désactiver ces notifications, les administrateurs d'interface peuvent ajouter <code dir="ltr">#siteNotice { display: none; }</code> à [[{{ns:8}}:Minerva.css]]. [https://phabricator.wikimedia.org/T138572][https://phabricator.wikimedia.org/T416644] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:19|la tâche soumise|les {{formatnum:19}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:19||s}} la semaine dernière]]. Par exemple, un problème concernant la section ''[[Special:RecentChanges|Spécial:Modifications récentes]]'' a été résolu. Auparavant, cliquer sur « Masquer » dans les filtres actifs entraînait la disparition du bouton « Afficher les nouvelles modifications depuis… », alors qu'il aurait dû rester visible. Ce bouton fonctionne désormais correctement. [https://phabricator.wikimedia.org/T406339] '''Actualités pour la contribution technique''' * Une nouvelle documentation est désormais disponible pour aider les rédacteurs à déboguer les fonctionnalités de recherche interne. Elle facilite le dépannage lorsque des pages n'apparaissent pas dans les résultats, lorsque le classement semble inattendu et lorsqu'il est nécessaire d'inspecter le contenu indexé, ce qui permet de mieux comprendre et d'analyser le comportement de la recherche. [[mw:Help:CirrusSearch/Debug|En savoir plus]]. [https://phabricator.wikimedia.org/T411169] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.16|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/08|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W08"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 16 février 2026 à 20:17 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30086330 --> == <span lang="en" dir="ltr">Tech News: 2026-09</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W09"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/09|Translations]] are available. '''Weekly highlight''' * [[mw:Special:MyLanguage/Edit check/Reference Check|Reference Check]] has been deployed to English Wikipedia, completing its rollout across all Wikipedias. The feature prompts newcomers to add a citation before publishing new content, helping reduce common citation-related reverts and improve verifiability. In A/B testing, the impact was substantial: newcomers shown Reference Check were approximately 2.2 times more likely to include a reference on desktop and about 17.5 times more likely on mobile web. [https://analytics.wikimedia.org/published/reports/editing/reference_check_ab_test_report_final_2025.html] '''Updates for editors''' * The [[mw:Special:MyLanguage/Extension:InterwikiSorting|InterwikiSorting extension]], which allowed for the [[m:Special:MyLanguage/Interwiki sorting order|sorting of interwiki links]], has been undeployed from Wikipedia. As a result, editors who had enabled interwiki link sorting in non-compact mode (full list format) will now see links reordered. The links moving forward will be listed in the alphabetical order of language code. [https://phabricator.wikimedia.org/T253764] * Later this week, people who are editing a page-section using the mobile visual editor, will notice a new "Edit full page" button. When tapped, you will be able to edit the entire article. This helps when the change you want to make is outside the section you initially opened. [https://phabricator.wikimedia.org/T387175][https://phabricator.wikimedia.org/T409112] * [[mw:Special:MyLanguage/Readers/Reader Experience|The Reader Experience team]] is inviting editors to assess whether dark mode should still be considered "beta" on their wiki, based on their experience of how well it functions on desktop and mobile. If the feature is deemed mature, editors can update the interface messages in <code dir=ltr>MediaWiki:skin-theme-description</code> and <code dir=ltr>MediaWiki:Vector-night-mode-beta-tag</code> to indicate that dark mode is ready and no longer considered beta. * The improved [[mw:Wikimedia_Apps/Team/iOS/Activity_Tab|Activity tab]] which displays user-insights is now available to all users of the Wikipedia iOS app (version 7.9.0 and later). Following earlier A/B testing that showed higher account creation among users with access to the feature, it has been rolled out to 100% of users along with some updates. The Activity tab now shows your edited articles in the timeline, offers editing impact insights like contribution counts and article view trends, and customization options to improve in-app experience for users. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:21}} community-submitted {{PLURAL:21|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, a bug that prevented [[mw:Special:MyLanguage/Extension:DiscussionTools|DiscussionTools]] from working on mobile has now been fixed, restoring full functionality. [https://phabricator.wikimedia.org/T415303] '''Updates for technical contributors''' * The [[m:Special:GlobalWatchlist|Global Watchlist]] lets you view your watchlists from multiple wikis on one page. The [[mw:Special:MyLanguage/Extension:GlobalWatchlist|extension]] that makes this possible continues to improve. The latest upgrade is the inclusion of a [[mw:Extension:GlobalWatchlist#hook|new hook]], <code dir=ltr>ext.globalwatchlist.rebuild</code>, which fires after each watchlist rebuild. This allows you to run gadgets and user scripts for the Special page. [https://phabricator.wikimedia.org/T275159] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.46/wmf.17|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/09|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W09"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 23 février 2026 à 20:03 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30119102 --> == Actualités techniques n° 2026-10 == <section begin="technews-2026-W10"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/10|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * Le [[m:Special:MyLanguage/Wikipedia 25/Easter egg experiments|mode Anniversaire]] Wikipedia 25 est maintenant disponible sur Wikipédia en français, anglais, betawi, breton, chinois, espagnol, gorontalo, indonésien, italien, luxembourgeois, madurais, néerlandais, sicilien, tchèque, thaï et vietnamien ! Cette campagne à temps limitée célèbre 25 ans de Wikipédia avec une mascotte : « Baby Globe », disponible sous la forme d'un réglage. Lorsque ce réglage est activé, Baby Globe est montrée sur [[m:Special:MyLanguage/Wikipedia 25/Easter egg experiments/article configuration|environ 2 500 articles]], attendant d'être découverte par des lecteurs. Chaque communauté peut choisir d'activer le mode Anniversaire par consensus et en demandant à un administrateur de le rendre disponible et de le personaliser via une [[m:Special:MyLanguage/Wikipedia 25/Easter egg experiments#Community Configuration Demo|configuration]] sur le wiki local. '''Actualités pour la contribution''' * Le [[:m:Special:MyLanguage/WMDE Technical Wishes/Sub-referencing|sous-référencement]], une nouvelle fonctionalité pour réutiliser des références avec des détails différents est maintenant disponible sur Wikipédia en suédois, polonais et [[:phab:T418209|quelques autres]]. Vous pouvez [[:m:Special:MyLanguage/WMDE Technical Wishes/Sub-referencing#test|essayer la fonctionalité]] sur ces projets ou sur testwiki et [https://en.wikipedia.beta.wmcloud.org/wiki/Sub-referencing betawiki]. Les retours des premiers essais sur Wikipédia en allemand ont été [[:m:Special:MyLanguage/WMDE Technical Wishes/Sub-referencing/Learnings|publiés dans un rapport]]. Contactez l'équipe de Wikimédia Allemagne si vous êtes [[:m:Talk:WMDE Technical Wishes/Sub-referencing#Pilot wikis|intéressés pour devenir un wiki pilote]]. * La [[mw:Special:MyLanguage/Help:Edit check#Paste check|vérification du collage clavier]] sera disponible sur tous les Wikipédias cette semaine. Cette fonctionalité avertit les nouveaux contributeurs qui collent du texte qu'ils n'ont probablement pas écrit de vérifier si laisser celui-ci risque de causer une violation du droit d'auteur. La vérification du collage clavier [[mw:Special:MyLanguage/Edit check/Tags|marque]] toutes les modifications où l'avertissement a été montré pour permettre leur vérification. Les administrateurs locaux peuvent configurer les différents aspects de cette fonctionalité à travers [[{{#special:EditChecks}}]]. Des [[mw:Special:MyLanguage/Edit check/Paste Check#A/B Experiment|études]] sur 22 wikis ont montré que cette vérification permet une réduction de 18% des annulations comparé au groupe de contrôle. Les traducteurs peuvent [https://translatewiki.net/w/i.php?title=Special%3ATranslate&group=ext-visualeditor-ve-mw-editcheck&filter=&optional=1&action=translate aider à traduire] cette fonctionalité. * <span lang="en" dir="ltr" class="mw-content-ltr">The [[mw:Special:MyLanguage/Readers/Reader Experience|Reader Experience team]] will be standardizing the user menu in the top right for all mobile users so that it is closer to the desktop experience. Currently this user menu is only visible to users with Advanced Mobile Controls (AMC) turned on. The only change is that a couple buttons previously in the left-side menu will move to the top right for users who do not have AMC turned on. This change is expected to go out March 9 and seeks to improve the user interface.</span> [https://phabricator.wikimedia.org/T413912] * À partir de la semaine du 2 mars, les emails envoyés lorsqu'une adresse email a été ajoutée, supprimée ou changée pour un compte changera pour adopter un formattage HTML beaucoup plus agréable et plus clair que le texte brut précédent. [https://phabricator.wikimedia.org/T410807] * Les notifications sont actuellement limitées à 2 000 entrées historiques par utilisateur et remontent à 2013 lorsque la fonctionnalité a été publiée. Le système va être modifié pour ne stocker que les notifications des 5 dernières années, mais jusqu'à 10 000 d'entre elles. Cela contribuera à la santé à long terme des infrastructures et à empêcher que les notifications plus récentes disparaissent trop tôt. [https://phabricator.wikimedia.org/T383948] * <span lang="en" dir="ltr" class="mw-content-ltr">The [[m:Special:GlobalWatchlist|Global Watchlist]] which lets you view your watchlists from multiple wikis on a single page continues to see improvements. The latest update improves label usage experience. The [[mw:Special:MyLanguage/Extension:GlobalWatchlist|extension]] now allows activating the [[mw:Special:MyLanguage/Manual:Language#Fallback languages|language fallback system]] for Wikidata items without labels in the viewed language, and showing those labels in the user’s preferred Wikidata language if no <code dir=ltr>uselang=</code> URL parameter is provided.</span> [https://phabricator.wikimedia.org/T373686][https://phabricator.wikimedia.org/T416111] * L'équipe Wikipédia Android a commencé un test beta de la [[mw:Special:MyLanguage/Readers/Information Retrieval/Phase 1|recherche hybride]] sur Wikipédia en grec. Cette recherche hybride supporte les requêtes sémantique et par mot clés, permettant aux utilisateurs de trouver ce qu'ils cherchent plus facilement. * Pour des raisons de sécurité, les membres de certains groupes sont [[m:Special:MyLanguage/Mandatory two-factor authentication for users with some extended rights|forcés d'avoir la double authentification]] (A2F) d'activée. Actuellement, l'A2F n'est nécessaire que pour utiliser les droits du groupe, et non pour en faire partie. Vu que ce système admet certaines failles, il sera [[phab:T418580|changé graduellement en mars]]. Les membres de ces groupes ne pourront plus désactiver la dernière méthose d'A2F sur leur compte, et il sera impossible d'ajouter des utilisateurs sans A2F à ces groupes. Il sera toujours possible de rajouter d'autres méthodes d'authentification et d'en enlever, tant qu'une est toujours activée. Dans la seconde moitié de mars, les utilisateurs sans A2F seront retirés de ces groupes. Cela s'applique aux administrateurs CentralNotice, aux vérificateurs d'utilisateurs, aux administrateurs d'interface, aux masqueurs, aux staff de Wikidata et Wikifonctions ainsi qu'aux bureaux IT et Confiance et sécurité de la WMF. Rien ne changera pour les autres utilisateurs. Voir la tâche liée pour le calendrier de déploiement. [https://phabricator.wikimedia.org/T418580] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:27|la tâche soumise|les {{formatnum:27}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:27||s}} la semaine dernière]]. Par exemple, le problème empêchant les utilisateurs de créer une instance dans [https://www.wikibase.cloud/ Wikibase.cloud] a maintenant été résolu. [https://phabricator.wikimedia.org/T416807] '''Actualités pour la contribution technique''' * <span lang="en" dir="ltr" class="mw-content-ltr">To help ensure [[mw:Special:MyLanguage/MediaWiki Product Insights/Responsible Reuse|fair use of infrastructure]], over the next month the Wikimedia Foundation will implement global API rate limits across our APIs. In early March, stricter limits will be applied to unidentified requests from outside Toolforge/WMCS and API requests that are made from web browsers. In April, higher limits will be applied to identified traffic. These limits are intentionally set as high as possible to minimise impact on the community. Bots running in Toolforge/WMCS or with the bot user right on any wiki should not be affected for now. However, all developers are advised to follow updated best practices. For more information, see [[mw:Special:MyLanguage/Wikimedia APIs/Rate limits|Wikimedia APIs/Rate limits]].</span> * <span lang="en" dir="ltr" class="mw-content-ltr">The Wikidata Query Service Linked Data Fragment (LDF) endpoint will be decommissioned in February. This endpoint served limited traffic, which was successfully migrated to other data access methods that were better suited to support existing use cases. The hardware used to support the LDF endpoint will be reallocated to support the ongoing backend migration efforts.</span> [https://phabricator.wikimedia.org/T415696] * Le nouvel analyseur syntaxique Parsoid [[mw:Special:MyLanguage/Parsoid/Parser Unification/Updates|continue d'être déployés sur plus de wikis]], améliorant la pérennité de la platforme et rendant plus facile l'ajout de nouvelles fonctionalités de lecture et de modification. Parsoid est maintenant l'analyseur par défaut sur 488 wikis de la WMF (268 Wikipédias), couvrant plus de 10% de toutes les lectures de pages Wikipédia. * Le processus et les critères pour [[Special:MyLanguage/Wikimedia Enterprise#Access|demander un accès exceptionnel]] au flux à fort volume de l'API ''Wikimédia Entreprise'' (sans coût pour des utilisations en rapport à notre mission) [[m:Talk:Wikimedia Enterprise#Exceptional access criteria|ont maintenant été publiés]]. Notre but est de donner une documentation plus claire et plus complète aux utilisateurs. * [https://techblog.wikimedia.org/ Le blog Tech], dédié à la communité technique de Wikimédia [https://techblog.wikimedia.org/2026/02/24/a-tech-blog-diff/ va migrer] vers [[diffblog:|Diff]], le blog pour les nouvelles et événements de la communauté. La migration devrait être terminée en Avril 2026, après quoi les nouveaux posts seront acceptés pour être publiés. Les lecteurs pourront lire les posts - anciens ou nouveaux - sur https://diff.wikimedia.org/. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.18|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/10|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W10"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 2 mars 2026 à 18:51 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30137798 --> == <span lang="en" dir="ltr">Tech News: 2026-11</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W11"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/11|Translations]] are available. '''Weekly highlight''' * [[m:Special:MyLanguage/Tech/Server switch|All wikis will be read-only]] for a few minutes on Wednesday, 25 March 2026 at [https://zonestamp.toolforge.org/1774450800 15:00 UTC]. This is for the datacenter server switchover backup tests, [[wikitech:Deployments/Yearly calendar|which happen twice a year]]. During the switchover, all Wikimedia website traffic is shifted from one primary data center to the backup data center to test availability and prevent service disruption even in emergencies. * Last week, all wikis had 2 hours of read-only time, and extended unavailability for user-scripts and gadgets. This was due to a security incident which has since been resolved. Work is ongoing to prevent re-occurrences. For current information please see the [[m:Steward's noticeboard#Statement on Meta about today's user script security incident|post on the Stewards' noticeboard]] ([[m:Special:MyLanguage/Wikimedia Foundation/Product and Technology/Product Safety and Integrity/March 2026 User Script Incident|translations]]). '''Updates for editors''' * Users facing multiple blocks on mobile will now see the reasons for each block separately, instead of a generic message. This helps them understand why they are blocked and what steps they can take to resolve the issue. For example, users affected for using common VPNs (such as [[Special:MyLanguage/Apple iCloud Private Relay|iCloud Private Relay]]) will receive clearer guidance on what they need to do to start editing again. [https://phabricator.wikimedia.org/T357118] * Later this week, [[mw:Special:MyLanguage/VisualEditor/Suggestion Mode|Suggestion Mode]] will become available as a beta feature within the visual editor at all Wikipedias. This feature proactively suggests various types of actions that people can consider taking to improve Wikipedia articles, and learn about related guidelines. The feature is locally configurable, and can also be locally expanded with custom Suggestions. Current settings can be seen at [[Special:EditChecks]] and there are [[mw:Special:MyLanguage/Help:Suggestion mode#For administrators %E2%80%93 local customization|instructions for how administrators can customize]] the links to point to local guidelines. The feature is connected to [[mw:Special:MyLanguage/Help:Edit check|Edit check]] which suggests improvements while someone is writing new content. In the future, the Editing team plans to evaluate the feature's impact with newcomers through a controlled experiment. [https://phabricator.wikimedia.org/T404600] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:23}} community-submitted {{PLURAL:23|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, the issue where the cursor became misaligned during the use of CodeMirror’s syntax highlighting, which makes wikitext and code easier to read, has now been fixed. This problem specifically affected users who defined a font rule in a custom stylesheet while creating a new topic with DiscussionTools. [https://phabricator.wikimedia.org/T418793] '''Updates for technical contributors''' * API rate limiting update: To help ensure [[mw:Special:MyLanguage/MediaWiki Product Insights/Responsible Reuse|fair use of infrastructure]], global API rate limits will be applied this week to requests without a compliant User-Agent that originate from outside Toolforge/WMCS and to unauthenticated requests made from web browsers. Higher limits will be applied to identified traffic in April. Bots running in Toolforge/WMCS or with the bot user right on any wiki should not be affected for now. However, all developers are advised to follow updated best practices. For more information, see [[mw:Special:MyLanguage/Wikimedia APIs/Rate limits|Wikimedia APIs/Rate limits]]. * The new GraphQL API has been released. The API was developed as a flexible alternative to select features of the Wikidata Query Service (WDQS), to improve developer experience and foster adaptability, and efficient data access. Try it out and [[d:Wikidata:Wikibase GraphQL#Feedback and development|give feedback]]. You can also [https://greatquestion.co/wikimediadeutschland/GraphQLAPI/apply sign up for usability tests]. * The [[m:Special:MyLanguage/Product and Technology Advisory Council/Unsupported Tools Working Group|PTAC Unsupported Tools Working Group]] continued improvements to [[commons:Special:MyLanguage/Commons:Video2commons#|Video2Commons]] in February, with fixes addressing authentication errors, large-file handling, task queue visibility, and clearer upload behavior. Work is still ongoing in some areas, including changes related to deprecated server-side uploads. Read [[m:Special:MyLanguage/Product and Technology Advisory Council/Unsupported Tools Working Group#February 2026|this update]] to learn more. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.46/wmf.19|MediaWiki]] '''In depth''' * The Article Guidance team invites experienced Wikipedia editors from selected [[mw:Special:MyLanguage/Article guidance/Pilot wikis and collaborators#Collaborators|pilot wikis]] and interested contributors from other Wikipedias to fill out this questionnaire which is available in [https://docs.google.com/forms/d/e/1FAIpQLSfmLeVWnxmsCbPoI_UF2jyRcn73WRGWCVPHzerXb4Cz97X_Ag/viewform English], [https://docs.google.com/forms/d/e/1FAIpQLSd6rzr4XXQw8r4024fE3geTPFe13M_6w7Mitj-YJi0sOlWTAw/viewform?usp=header Arabic], [https://docs.google.com/forms/d/e/1FAIpQLSdok3-RfB18lcugYTUMGkpwmqG_8p760Wv4dCXitOXOszjUDw/viewform?usp=header Bengali], [https://docs.google.com/forms/d/e/1FAIpQLSfjTfYp4jEo0akA4B1e-Nfg3QZPCudUjhJzHzzDi6AHyAaMGA/viewform?usp=header Japanese], [https://docs.google.com/forms/d/e/1FAIpQLScteVoI29Aue4xc72dekk-6RYtvmMgQxzMI900UOawrFrSTWg/viewform?usp=header Portuguese], [https://docs.google.com/forms/d/e/1FAIpQLSetdxnYwL3ub2vqA7awCg5hJZPMIYcDPaiTe12rY9h0GYnVlw/viewform?usp=header Persian], and [https://docs.google.com/forms/d/e/1FAIpQLScNvfJF-Ot-4pzA4qAN771_0QDJ4Li19YcUsaTgSKW8Nc7U_Q/viewform?usp=header Turkish]. Your answers will help the team customize guidance for less experienced editors and help them learn community policies and practices while creating an article. Learn more [[mw:Special:MyLanguage/Article guidance|on the project page]]. '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/11|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W11"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 9 mars 2026 à 19:52 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30213008 --> == <span lang="en" dir="ltr">Tech News: 2026-12</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W12"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/12|Translations]] are available. '''Updates for editors''' * The [[mw:Special:MyLanguage/Help:Extension:CodeMirror|{{int:codemirror-beta-feature-title}}]] beta feature, also known as [[mw:Special:MyLanguage/Extension:CodeMirror|CodeMirror 6]], has been used for wikitext syntax highlighting since November 2024. It will be promoted out of beta by May 2026 in order to bring improvements and new [[mw:Special:MyLanguage/Help:Extension:CodeMirror#Features|features]] to all editors who use the standard syntax highlighter. If you have any questions or concerns about promoting the feature out of beta, [[mw:Special:MyLanguage/Help talk:Extension:CodeMirror|please share]]. [https://phabricator.wikimedia.org/T259059] * Some changes to local user groups are performed by stewards on Meta-Wiki and logged there only. Now, interwiki rights changes will be logged both on Meta-Wiki and the wiki of the target user to make it easier to access a full record of user's rights changes on a local wiki. Past log entries for such changes will be backfilled in the coming weeks. [https://phabricator.wikimedia.org/T6055] * On wikis using [[m:Special:MyLanguage/Flagged Revisions|Flagged Revisions]], the number of pending changes shown on [[{{#Special:PendingChanges}}]] previously counted pages which were no longer pending review, because they have been removed from the system without being reviewed, e.g. due to being deleted, moved to a different namespace, or due to wiki configuration changes. The count will be correct now. On some wikis the number shown will be much smaller than before. There should be no change to the list of pages itself. [https://phabricator.wikimedia.org/T413016] * Wikifunctions composition language has been rewritten, resulting in a new version of the language. This change aims to increase service stability by reducing the orchestrator's memory consumption. This rewrite also enables substantial latency reduction, code simplification, and better abstractions, which will open the door to later feature additions. Read more about [[f:Special:MyLanguage/Wikifunctions:Status updates/2026-03-11|the changes]]. * Users can now sort search results alphabetically by page title. The update gives an additional option to finding pages more easily and quickly. Previously, results could be sorted by Edit date, Creation date, or Relevance. To use the new option, open 'Advanced Search' on the search results page and select 'Alphabetically' under 'Sorting Order'. [https://phabricator.wikimedia.org/T403775] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:28}} community-submitted {{PLURAL:28|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, the bug that prevented UploadWizard on Wikimedia Commons from importing files from Flickr has now been fixed. [https://phabricator.wikimedia.org/T419263] '''Updates for technical contributors''' * A new special page, [[{{#special:LintTemplateErrors}}]], has been created to list transcluded pages that are flagged as containing lint errors to help users discover them easily. The list is sorted by the number of transclusions with errors. For example: [[{{#special:LintTemplateErrors}}/night-mode-unaware-background-color]]. [https://phabricator.wikimedia.org/T170874] * Users of the [[mw:Special:MyLanguage/Help:Extension:CodeMirror|{{int:codemirror-beta-feature-title}}]] beta feature have been using [[mw:Special:MyLanguage/Extension:CodeMirror|CodeMirror]] instead of [[mw:Special:MyLanguage/Extension:CodeEditor|CodeEditor]] for syntax highlighting when editing JavaScript, CSS, JSON, Vue and Lua content pages, for some time now. Along with promoting CodeMirror 6 out of beta, the plan is to replace CodeEditor as the standard editor for these content models by May 2026. [[mw:Special:MyLanguage/Help talk:Extension:CodeMirror|Feedback or concerns are welcome]]. [https://phabricator.wikimedia.org/T419332] * The [[mw:Special:MyLanguage/Extension:CodeMirror|CodeMirror]] JavaScript modules will soon be upgraded to CodeMirror 6. Leading up to the upgrade, loading the <code dir=ltr>ext.CodeMirror</code> or <code dir=ltr>ext.CodeMirror.lib</code> modules from gadgets and user scripts was deprecated in July 2025. The use of the <code dir=ltr>ext.CodeMirror.switch</code> hook was also deprecated in March 2025. Contributors can now make their scripts or gadgets compatible with CodeMirror 6. See the [[mw:Special:MyLanguage/Extension:CodeMirror#Gadgets and user scripts|migration guide]] for more information. [https://phabricator.wikimedia.org/T373720] * The MediaWiki Interfaces team is expanding coverage of REST API module definitions to include [[mw:Special:MyLanguage/API:REST API/Extensions|extension APIs]]. REST API modules are groups of related endpoints that can be independently managed and versioned. Modules now exist for [https://phabricator.wikimedia.org/T414470 GrowthExperiments] and [https://phabricator.wikimedia.org/T419053 Wikifunctions] APIs. As we migrate extension APIs to this structure, documentation will move out of the main MediaWiki OpenAPI spec and REST Sandbox view, and will instead be accessible via module-specific options in the dropdown on the [https://test.wikipedia.org/wiki/Special:RestSandbox REST Sandbox] (i.e., [[{{#Special:RestSandbox}}]], available on all wiki projects). * The [[mw:Special:MyLanguage/Extension:Scribunto|Scribunto]] extension provides different pieces of information about the wiki where the module is being used via the [[mw:Special:MyLanguage/Extension:Scribunto/Lua reference manual|mw.site]] library. Starting last week, the library also provides a [[mw:Special:MyLanguage/Extension:Scribunto/Lua reference manual#mw.site.wikiId|way]] of accessing the [[mw:Special:MyLanguage/Manual:Wiki ID|wiki ID]] that can be used to facilitate cross-wiki module maintenance. [https://phabricator.wikimedia.org/T146616] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.46/wmf.20|MediaWiki]] '''In depth''' * The [[m:Special:MyLanguage/Coolest Tool Award|2026 Coolest Tool Award]] celebrating outstanding community tools, is now open for nominations! Nominate your favorite tool using the [https://wikimediafoundation.limesurvey.net/435684?lang=en nomination survey] form by 23 March 2026. For more information on privacy and data handling, please see the [[foundation:Special:MyLanguage/Legal:Coolest_Tool_Award_2026_Survey_Privacy_Statement|survey privacy statement]]. '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/12|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W12"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 16 mars 2026 à 20:35 (CET) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30260505 --> == <span lang="en" dir="ltr">Upcoming deployment of CampaignEvents extension to Wikibooks</span> == <div lang="en" dir="ltr"> <section begin="message"/> Hello everyone, We are writing to inform you that the [[mw:Help:Extension:CampaignEvents|CampaignEvents extension]] will be deployed to all Wikibooks projects during the week of '''23 March 2026'''. This follows last year’s broader rollout across Wikimedia projects. We realized that Wikibooks was not included at the time, and we’re now addressing that to ensure consistency across all communities. The CampaignEvents extension provides tools to support event and campaign organization on-wiki, including features like on-wiki event registration and collaboration lists(global event list). We welcome any questions, feedback, or concerns you may have. We are also happy to support anyone interested in trying out the tools. ''Apologies if this message is not in your preferred language. If you’re able to help translate it for your community, please feel free to do so.'' <section end="message"/> </div> <bdi lang="en" dir="ltr">[[User:Udehb-WMF|Udehb-WMF]] ([[User talk:Udehb-WMF|discussion]]) 19 mars 2026 à 19:22 (CET)</bdi> <!-- Message envoyé par User:Udehb-WMF@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=User:Udehb-WMF/sandbox/MM_target&oldid=30284073 --> == <span lang="en" dir="ltr">Tech News: 2026-13</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W13"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/13|Translations]] are available. '''Weekly highlight''' * Wikimedia site users can now log in without a password using passkeys. This is a secure method supported by fingerprint, facial recognition, or PIN. With this change, all users who opt for passwordless login will find it easier, faster, and more secure to log in to their accounts using any device. The new passkey login option currently appears as an autofill suggestion in the username field. An additional [[phab:T417120|"Log in with passkey" button]] will soon be available for users who have already registered a passkey. This update will improve security and user experience. The [[c:File:Passwordless_login_screencast.webm|screen recording]] demonstrates the passwordless login process step by step. * [[m:Special:MyLanguage/Tech/Server switch|All wikis will be read-only]] for a few minutes on Wednesday, 25 March 2026 at [https://zonestamp.toolforge.org/1774450800 15:00 UTC]. This is for the datacenter server switchover backup tests, [[wikitech:Deployments/Yearly calendar|which happen twice a year]]. During the switchover, all Wikimedia website traffic is shifted from one primary data center to the backup data center to test availability and prevent service disruption even in emergencies. '''Updates for editors''' * Wikimedia site users can now export their notifications older than 5 years using a [[toolforge:echo-chamber|new Toolforge tool]]. This will ensure that users retain their important notifications and avoid them being lost based on the planned change to delete notifications older than 5 years, as previously announced. [https://phabricator.wikimedia.org/T383948] * Wikipedia editors in Indonesian, Thai, Turkish, and Simple English now have access to Special:PersonalDashboard. This is an [[mw:Special:MyLanguage/Moderator Tools/Dashboard|early version of an experience]] that introduces newer editors to patrolling workflows, making it easier for them to move from making edits to participating in more advanced moderation work on their project. [https://phabricator.wikimedia.org/T402647] * The [[Special:Block]] now has two minor interface changes. Administrators can now easily perform indefinite blocks through a dedicated radio button in the expiry section. Also, choosing an indefinite expiry provides a different set of common reasons to select from, which can be changed at: [[MediaWiki:Ipbreason-indef-dropdown]]. [https://phabricator.wikimedia.org/T401823] * Mobile editors [[mw:Special:MyLanguage/Contributors/Account Creation Experiments#Logged-out|at several wikis]] can now see an improved logged-out edit warning, thanks to the recent updates from the Growth team. These changes released last week are part of ongoing efforts and tests to enhance [[mw:Special:MyLanguage/Contributors/Account Creation Experiments|account creation experience on mobile]] and then increase participation. [https://phabricator.wikimedia.org/T408484] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:36}} community-submitted {{PLURAL:36|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, the bug that prevented mobile web users from seeing the block information when affected by multiple blocks has been fixed. They can now see messages of all the blocks currently affecting them when they access Wikipedia. '''Updates for technical contributors''' * Images built using Toolforge will soon get the upgraded buildpacks version, bringing support for newer language versions and other upstream improvements and fixes. If you use Toolforge Build Service, review the recent [https://lists.wikimedia.org/hyperkitty/list/cloud-announce@lists.wikimedia.org/thread/EMYTA32EV2V5SQ2JIEOD2CL66YFIZEKV/ cloud-announce email] and update your build configuration as necessary to ensure your tools are compatible. [https://wikitech.wikimedia.org/w/index.php?title=Help:Toolforge/Building_container_images&oldid=2392097#Buildpack_environment_upgrade_process][https://phabricator.wikimedia.org/T380127] * The [https://api.wikimedia.org/wiki/Main_Page API Portal] documentation wiki will shut down in June 2026. API keys created on the API Portal will continue to work normally. api.wikimedia.org endpoints will be deprecated gradually starting in July 2026. Documentation on the API Portal is moving to [[mw:Wikimedia APIs|mediawiki.org]]. Learn more on the [[wikitech:API Portal/Deprecation|project page]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.46/wmf.21|MediaWiki]] '''In depth''' * [[m:Special:MyLanguage/WMDE Technical Wishes|WMDE Technical Wishes]] is considering improvements to [[m:WMDE Technical Wishes/References/VisualEditor automatic reference names|automatically generated reference names in VisualEditor]]. Please check out the [[m:WMDE Technical Wishes/References/VisualEditor automatic reference names#Proposed solutions|proposed solutions]] and participate in the [[m:Talk:WMDE Technical Wishes/References/VisualEditor automatic reference names#Request for comment|request for comment]]. '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/13|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W13"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 23 mars 2026 à 17:51 (CET) <!-- Message envoyé par User:UOzurumba (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30268305 --> == Actualités techniques n° 2026-14 == <section begin="technews-2026-W14"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/14|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * Le version Beta de [[abstract:|Abstract Wikipedia]], un nouveau projet Wikimédia indépendant du langage, a été lancée la semaine dernière. Ce projet permet aux communautés de construire des articles Wikipédia dans leur langue natale, qui peuvent directement être lus par les autres utilisateurs et utilisatrices dans leur propre langage. Le wiki fonctionne grâce à des instructions de Wikifunctions et au contenu structuré issu de Wikidata. [[:f:Special:MyLanguage/Wikifunctions:Status updates/2026-03-26|En savoir plus]]. '''Actualités pour la contribution''' * L'équipe Croissance mène un test A/B afin d'évaluer l'effet d'un message plus clair et plus convivial encourageant à la création de comptes sur les wikis. Actuellement, lorsqu'un utilisateur mobile non connecté lance la modification, un message d'avertissement s'affiche, pouvant paraître abrupt et décourageant. Il présente également la modification par compte temporaire comme option par défaut, au lieu d'inciter à la création d'un compte. Le test est mené sur dix Wikipédia, dont les versions en arabe, français, espagnol et allemand. [[mw:Special:MyLanguage/Contributors/Account Creation Experiments#2. Improve logged-out warning message (T415160)|En savoir plus]]. * L'équipe des applications Wikimédia sollicite vos commentaires sur [[mw:Special:MyLanguage/Wikimedia Apps/Team/Future of Editing on the Mobile Apps|comment devrait fonctionner l'édition dans les applications mobiles Wikipédia]]. La discussion porte sur l'amélioration de l'accès aux outils d'édition lorsque les utilisateurs appuient sur « Modifier ». Cette initiative s'inscrit dans un effort plus large visant à offrir aux lecteurs intéressés par la contribution une expérience utilisateur plus intuitive. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:45|la tâche soumise|les {{formatnum:45}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:45||s}} la semaine dernière]]. Par exemple, un problème avec la récupération de citations à partir du site d'archive de journaux [https://www.newspapers.com Newspapers.com], qui ne fonctionnait plus en raison d'un blocage des requêtes [[mw:Special:MyLanguage/Citoid|Citoid]], a maintenant été résolu. [https://phabricator.wikimedia.org/T419903] '''Actualités pour la contribution technique''' * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.22|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/14|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W14"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 30 mars 2026 à 21:25 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30329462 --> == Action Required: Update templates/modules for electoral maps (Migrating from P1846 to P14226) == Hello everyone, This is a notice regarding an ongoing data migration on Wikidata that may affect your election-related templates and Lua modules (such as <code>Module:Itemgroup/list</code>). '''The Change:'''<br /> Currently, many templates pull electoral maps from Wikidata using the property [[:d:Property:P1846|P1846]], combined with the qualifier [[:d:Property:P180|P180]]: [[:d:Q19571328|Q19571328]]. We are migrating this data (across roughly 4,000 items) to a newly created, dedicated property: '''[[:d:Property:P14226|P14226]]'''. '''What You Need To Do:'''<br /> To ensure your templates and infoboxes do not break or lose their maps, please update your local code to fetch data from [[:d:Property:P14226|P14226]] instead of the old [[:d:Property:P1846|P1846]] + [[:d:Property:P180|P180]] structure. A [[m:Wikidata/Property Migration: P1846 to P14226/List|list of pages]] was generated using Wikimedia Global Search. '''Deadline:'''<br /> We are temporarily retaining the old data on [[:d:Property:P1846|P1846]] to allow for a smooth transition. However, to complete the data cleanup on Wikidata, the old [[:d:Property:P1846|P1846]] statements will be removed after '''May 1, 2026'''. Please update your modules and templates before this date to prevent any disruption to your wiki's election articles. Let us know if you have any questions or need assistance with the query logic. Thank you for your help! [[User:ZI Jony|ZI Jony]] using [[Utilisateur:MediaWiki message delivery|MediaWiki message delivery]] ([[Discussion utilisateur:MediaWiki message delivery|discussion]]) 3 avril 2026 à 19:11 (CEST) <!-- Message envoyé par User:ZI Jony@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Distribution_list/Non-Technical_Village_Pumps_distribution_list&oldid=29941252 --> == Actualités techniques n° 2026-15 == <section begin="technews-2026-W15"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/15|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * L’[[mw:Special:MyLanguage/Help:Extension:CampaignEvents|extension CampaignEvents]] comprend désormais une nouvelle fonctionnalité de définition d’objectifs de groupe, permettant aux organisateurs de définir et de suivre les objectifs de l’événement, tels que le nombre d’articles créés et de contributeurs participants en temps réel. De même, les participants peuvent travailler vers des cibles communes et voir leur impact collectif au fur et à mesure que l’événement se déroule. Cette fonctionnalité est désormais disponible sur tous les wikis Wikimedia. Pour en savoir plus, consultez [[mw:Special:MyLanguage/Help:Extension:CampaignEvents/Registration/Collaborative contributions#Goal setting|la documentation]]. * [[File:Maki-gift-15.svg|12px|link=|class=skin-invert|Concerne un souhait]] La nouvelle fonctionnalité d'[[mw:Special:MyLanguage/Help:Watchlist labels|étiquettes de liste de suivi]] (annoncée dans les [[m:Special:MyLanguage/Tech/News/2026/07|Actualités techniques 2026-07 ]]) est désormais disponible via l'ÉditeurVisuel, l'éditeur de code et l'«étoile de suivi»(ou le lien de suivi, pour les habillages qui n'ont pas d'icône d'étoile). Auparavant, il n'était possible d'attribuer des étiquettes que via [[Special:EditWatchlist|Modifier la liste de suivi]]. Dans ces trois emplacements, il s'agit d'un nouveau champ situé après le champ d'expiration. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:23|la tâche soumise|les {{formatnum:23}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:23||s}} la semaine dernière]]. Par exemple, le problème où les pages de discussion sur mobile avec Parsoid sont inutilisables après les en-têtes de section vides, a maintenant été résolu. [https://phabricator.wikimedia.org/T419171] '''Actualités pour la contribution technique''' * La [[m:Special:MyLanguage/WMDE Technical Wishes/Sub-referencing|fonctionnalité de sous-référencement]], qui permet aux contributeurs d'ajouter des détails à une référence existante sans la dupliquer, sera progressivement déployée sur [[phab:T414094|davantage de wikis]] plus tard cette année. Les wikis utilisant le gadget [[mw:Special:MyLanguage/Reference Tooltips|Reference Tooltips]] sont encouragés à mettre à jour leur version (généralement sur [[m:MediaWiki:Gadget-ReferenceTooltips.js|MediaWiki:Gadget-ReferenceTooltips.js]] comme indiqué [https://en.wikipedia.org/w/index.php?diff=1344408362 ici]) pour assurer la compatibilité. D'autres gadgets liés aux références pourraient également être affectés. [https://phabricator.wikimedia.org/T416304] * Toutes les éditions de Wikinews seront fermées et passeront en mode lecture seule le 4 mai 2026. Le contenu restera accessible, mais aucune nouvelle modification ni aucun nouvel article ne pourra être ajouté. Cette fermeture a été approuvée par le Conseil d'administration de la Fondation Wikimedia à la suite de discussions prolongées. [[m:Wikimedia Foundation Board noticeboard#Board of Trustees Approves Closure of Wikinews|En savoir plus]]. * L'[[:mw:Special:MyLanguage/API:Action API|API d'action]] a proposé plusieurs formats pour les résultats demandés. L'un d'entre eux, <bdi lang="zxx" dir="ltr"><code><nowiki>format=php</nowiki></code></bdi>, sera bientôt supprimé. Veuillez vous assurer que vos scripts ou robots utilisent le [[mw:Special:MyLanguage/API:Data formats#Output|format JSON]]. Cette suppression devrait affecter très peu de scripts et de robots. [https://phabricator.wikimedia.org/T118538] * La page [[Special:NamespaceInfo|Special:NamespaceInfo]] inclut désormais les alias d'espace de noms. Par exemple «WP» pour l'espace de noms ''Projet'' (''Wikipédia'') sur la Wikipédia en allemand. [https://phabricator.wikimedia.org/T381455] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.23|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/15|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W15"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 6 avril 2026 à 18:19 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30362761 --> == <span lang="en" dir="ltr">Tech News: 2026-16</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W16"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/16|Translations]] are available. '''Weekly highlight''' * Experienced editors are invited to [https://b24e11a4f1.catalyst.wmcloud.org/wiki/Main_Page test] the [[mw:Special:MyLanguage/Article guidance|Article guidance]] feature, designed to help less-experienced editors create well-structured, policy-compliant Wikipedia articles. Testing instructions are [[mw:Special:MyLanguage/Article guidance/Test feature guide|available]]. Also, after reviewing [https://b24e11a4f1.catalyst.wmcloud.org/wiki/Category:Pages_using_article_guidance the outlines], please provide feedback on the [[mw:Talk:Article guidance|project talk page]]. Based on your input, the feature will be refined and transferred to the pilot Wikipedias to translate and adapt. Check out [[c:File:Article Guidance workflow demo - April 2026.webm|the video]] explaining the feature. '''Updates for editors''' * On most wikis, all autoconfirmed users can now use [[Special:ChangeContentModel|Special:ChangeContentModel]] page to [[mw:Special:MyLanguage/Help:ChangeContentModel|create new pages with custom content models]], such as mass message lists, making custom page formats more accessible. Check [[Special:ListGroupRights|Special:ListGroupRights]] for the status of your wiki. [https://phabricator.wikimedia.org/T248294] * The Growth team has launched an [[mw:Special:MyLanguage/Contributors/Account_Creation_Experiments|account creation experiment]] to evaluate whether adding an account creation button to the mobile web header increases new account registrations and encourages more mobile users to contribute to the wikis. The experiment is currently live on Hindi, Indonesian, Bengali, Thai, and Hebrew Wikipedia, and targets 10% of logged-out mobile web users. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:30}} community-submitted {{PLURAL:30|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, an issue where VisualEditor could get stuck loading on Windows devices with animations turned off, has now been fixed. [https://phabricator.wikimedia.org/T382856] '''Updates for technical contributors''' * Starting later this week, {{int:group-abusefilter}} who have the [[mw:Special:MyLanguage/Help:Extension:CodeMirror|{{int:codemirror-beta-feature-title}}]] beta feature enabled will have [[mw:Special:MyLanguage/Extension:CodeMirror|CodeMirror]] instead of [[mw:Special:MyLanguage/Extension:CodeEditor|CodeEditor]] as the editor at [[Special:AbuseFilter|Special:AbuseFilter]]. This is part of the broader effort to make the user experience more consistent across all editors. [https://phabricator.wikimedia.org/T399673][https://phabricator.wikimedia.org/T419332] * Tools and bots that access the [[mw:Special:MyLanguage/Notifications/API|Notifications API]] (<bdi lang="zxx" dir="ltr"><code><nowiki>action=query&meta=notifications</nowiki></code></bdi>) will need to update their OAuth or BotPassword grants to also include access to private notifications. [https://phabricator.wikimedia.org/T421991] * Due to a library upgrade, listings on category pages may be displayed out of order starting on Monday, 20th April. A migration script will be run to correct this, and will take hours to days depending on the size of the wiki (up to a week for English Wikipedia). [https://phabricator.wikimedia.org/T422544] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.46/wmf.24|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/16|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W16"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 13 avril 2026 à 17:19 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30380527 --> == Actualités techniques n° 2026-17 == <section begin="technews-2026-W17"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/17|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * Après deux ans de développement, la version [[mw:Special:MyLanguage/Help:Extension:CodeMirror|{{int:codemirror-beta-feature-title}}]], également connue sous le nom de [[mw:Special:MyLanguage/Extension:CodeMirror|CodeMirror 6]], sortira de sa phase bêta le mardi 21 avril. Elle offrira une meilleure lisibilité du code et du wikitext, une réduction des fautes de frappe et d'autres [[mw:Special:MyLanguage/Help:Extension:CodeMirror|avantages]] à tous les utilisateurs du surligneur de syntaxe standard. Un grand merci au bénévole [https://phabricator.wikimedia.org/p/Bhsd/ Bhsd] qui a développé de nombreuses nouvelles fonctionnalités, notamment [[mw:Special:MyLanguage/Help:Extension:CodeMirror#Code folding|le repliement de code]], [[mw:Special:MyLanguage/Help:Extension:CodeMirror#Autocompletion|la saisie semi-automatique]] et [[mw:Special:MyLanguage/Help:Extension:CodeMirror#Linting|l'analyse statique du code]]. [https://phabricator.wikimedia.org/T259059] * Une mise à jour majeure de l'application Wikipédia pour iOS est en cours de déploiement, en restructurant l'interface pour s'harmoniser avec le tout nouveau design visuel "Liquid Glass" d'Apple. [https://apps.apple.com/us/app/wikipedia/id324715238 Télécharger la dernière version] et découvrez les nouveautés. '''Actualités pour la contribution''' * [[mw:Special:MyLanguage/Readers/Reader Experience/WE3.3.4 Reading lists|Les listes de lecture]] est une fonctionnalité qui permet aux lecteurs d'enregistrer des articles dans une liste pour les lire ultérieurement. Cette fonctionnalité est actuellement en version bêta sur les Wikipédias en arabe, français, indonésien, vietnamien et chinois, et activée par défaut pour tous les nouveaux comptes sur toutes les Wikipédias. * Une expérimentation visant à étendre [[mw:Special:MyLanguage/Readers/Reader Growth/Mobile page previews|les aperçus de page au web mobile]] sera lancée la semaine du 20 avril sur les versions arabe, anglaise, française, italienne, polonaise et vietnamienne de Wikipédia. Les aperçus de page sont des fenêtres contextuelles affichant une miniature, un premier paragraphe et un lien bleu permettant d'ouvrir l'article complet, facilitant ainsi la découverte de contenu. Cette fonctionnalité est déjà disponible sur ordinateur et dans les applications. [[m:Special:MyLanguage/List of experiments in Product and Technology#Template|En savoir plus sur cette expérimentation et d'autres]]. * Sur plusieurs wikis, les contributeurs connectés qui n'ont pas [[mw:Special:MyLanguage/Help:Email confirmation|confirmé leur adresse électronique]] peuvent désormais voir une bannière les invitant à le faire. La confirmation de l'adresse électronique permet à un utilisateur de récupérer l'accès à son compte en cas de perte. [[mw:Special:MyLanguage/Product Safety and Integrity/Account Security#Encouraging users to confirm their email addresses|En savoir plus]]. [https://phabricator.wikimedia.org/T421366] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:15|la tâche soumise|les {{formatnum:15}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:15||s}} la semaine dernière]]. Par exemple, un problème qui entraînait des ralentissements lors de la modification de très grandes pages wiki dans l'éditeur wikitext de 2017, des problèmes de chargement, de prévisualisation et de défilement, ainsi que des problèmes de performance lors de la sélection, de la découpe ou du collage de contenu, a maintenant été résolu. [https://phabricator.wikimedia.org/T184857] '''Actualités pour la contribution technique''' * Dans le cadre de la promotion de [[mw:Special:MyLanguage/Help:Extension:CodeMirror|CodeMirror]] à partir d'une fonctionnalité bêta, tous les utilisateurs se serviront de [[mw:Special:MyLanguage/Extension:CodeMirror|CodeMirror]] au lieu de [[mw:Special:MyLanguage/Extension:CodeEditor|CodeEditor]] pour la coloration syntaxique lors de l'édition de pages de contenu JavaScript, CSS, JSON, Vue et Lua. [https://phabricator.wikimedia.org/T419332] * <span class="mw-translate-fuzzy">Le service <code>mirrors.wikimedia.org</code> pour les utilisateurs de Debian et Ubuntu sera définitivement arrêté le 15 mai. Le matériel du serveur sera remplacé par des solutions plus performantes. Certains utilisateurs devront peut-être migrer vers un autre serveur qui ne devra prendre qu'une minute. [https://lists.wikimedia.org/hyperkitty/list/wikitech-l@lists.wikimedia.org/thread/LJYRIS4WB66HIRCAO4GIDTXCMDVZRBMA/ Vous pouvez en savoir plus].</span> [https://phabricator.wikimedia.org/T416707] * Les tables <bdi lang="zxx" dir="ltr"><code><nowiki>image</nowiki></code></bdi> et <bdi lang="zxx" dir="ltr"><code><nowiki>oldimage</nowiki></code></bdi> seront supprimées de [[wikitech:Help:Wiki Replicas|wikireplicas]]. Si vos outils ou requêtes accèdent directement à <bdi lang="zxx" dir="ltr"><code><nowiki>image</nowiki></code></bdi> ou <bdi lang="zxx" dir="ltr"><code><nowiki>oldimage</nowiki></code></bdi>, veuillez les mettre à jour pour utiliser les tables <bdi lang="zxx" dir="ltr"><code><nowiki>file</nowiki></code></bdi> et <bdi lang="zxx" dir="ltr"><code><nowiki>filerevision</nowiki></code></bdi> avant le 28 mai. [https://phabricator.wikimedia.org/T28741] * Suite à la récente mise en place de limites de débit globales pour les API non identifiées, la Fondation Wikimedia poursuit ses efforts pour garantir [[mw:Special:MyLanguage/MediaWiki Product Insights/Responsible Reuse|une utilisation équitable de l'infrastructure]] en appliquant des limites globales au trafic des API identifiées à partir de la dernière semaine d'avril. Ces limites sont volontairement fixées au niveau le plus élevé possible afin de minimiser l'impact sur la communauté. Les bots exécutés dans Toolforge/WMCS ou disposant des droits d'utilisateur de bot sur un wiki ne devraient pas être affectés pour le moment. Toutefois, il est conseillé à tous les développeurs de suivre les bonnes pratiques mises à jour. Pour plus d'informations, consultez la page [[mw:Special:MyLanguage/Wikimedia APIs/Rate limits|API Wikimedia/Limites de débit]] et la [[mw:Special:MyLanguage/Wikimedia APIs/Rate limits/FAQ|Foire aux questions]]. * L'[[mw:Special:MyLanguage/Attribution API|API d'attribution]] est désormais disponible en [[mw:Special:MyLanguage/Wikimedia APIs/Stability policy|version bêta]]. Elle récupère les informations nécessaires pour créditer les articles et les fichiers multimédias de Wikimedia, quel que soit leur lieu d'utilisation. La documentation de référence est disponible sur la page dédiée au Sandbox REST, accessible sur tous les wikis Wikimedia (comme [https://en.wikipedia.org/w/index.php?api=attribution.v0-beta&title=Special%3ARestSandbox le sandbox REST de Wikipédia en anglais]). N'hésitez pas à partager vos commentaires sur la [[mw:Talk:Attribution API|page de discussion du projet]]. * Il n'y aura pas de nouvelle version de MediaWiki cette semaine. '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/17|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W17"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 20 avril 2026 à 17:00 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30432763 --> == Request for comment (global AI policy) == <bdi lang="en" dir="ltr" class="mw-content-ltr"> Apologies for writing in English. {{int:Please-translate}} A [[:m:Requests for comment/Artificial intelligence policy|request for comment]] is currently being held to decide on a global AI policy. {{int:Feedback-thanks-title}} [[Utilisateur:MediaWiki message delivery|MediaWiki message delivery]] ([[Discussion utilisateur:MediaWiki message delivery|discussion]]) 26 avril 2026 à 02:57 (CEST) </bdi> <!-- Message envoyé par User:Codename Noreste@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Distribution_list/Global_message_delivery&oldid=30424282 --> == Actualités techniques n° 2026-18 == <section begin="technews-2026-W18"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/18|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * Un changement dans la manière dont les utilisateurs et utilisatrices sont automatiquement confirmés est en cours pour améliorer la protection contre le vandalisme. Actuellement, il suffit d’avoir un compte depuis quelques jours avec quelques contributions pour être ajouté au groupe [[{{int:grouppage-autoconfirmed/{{CONTENTLANGUAGE}}}}|{{int:group-autoconfirmed}}]]. Cette configuration tend à être exploitée par certains vandales qui créent des comptes et commencent à les utiliser après un certain temps. Pour réduire ce problème, la configuration va changer la semaine prochaine afin que l’âge du compte minimum pour être confirmé automatiquement ne soit calculé qu’à partir de la première modification, au lieu de la date d’inscription. L’âge minimum du compte restera le même, c’est seulement le point de départ pour calculer cet âge qui change. Ce changement ne sera déployé que sur les wikis qui nécessitent au moins une contribution pour satisfaire les conditions de confirmation automatique. [https://phabricator.wikimedia.org/T418484] * Tous les utilisateurs et utilisatrices de Wikipédia avec un nouveau compte et ceux qui ont activé l’option « activer automatiquement la plupart des fonctionnalités bêta » peuvent désormais utiliser la fonctionnalité bêta de [[mw:Special:MyLanguage/Readers/Reader Experience/WE3.3.4 Reading lists|listes de lecture]] pour enregistrer des articles à lire plus tard. Cela permet d’organiser les lectures qui nous intéressent à un endroit unique pour y accéder facilement. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:30|la tâche soumise|les {{formatnum:30}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:30||s}} la semaine dernière]]. Par exemple, le problème avec les images d’infoboite qui avaient une marge intérieure immense dans Firefox a été corrigé. [https://phabricator.wikimedia.org/T423676] '''Actualités pour la contribution technique''' * Pour rappel, la limite globale d’accès à l’API sera appliquée cette semaine pour identifier le trafic de l’API. Le but est d’aider à garantir un [[mw:MediaWiki Product Insights/Responsible Reuse|accès équitable à l’infrastructure]]. Les robots qui s’exécutent dans Toolforge ou WMCS, ou avec le droit utilisateur ''robot'' sur les wikis, ne devraient pas être affectés pour le moment. Cependant, il est conseillé à tous les développeurs et développeuses de se conformer aux nouvelles bonnes pratiques à suivre. Pour plus d’informations, notamment la limite globale d’accès effective, consultez [[mw:Wikimedia APIs/Rate limits|la page sur la limite d’accès des API de Wikimedia]] et les [[mw:Wikimedia APIs/Rate limits/FAQ|questions-réponses]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.26|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/18|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W18"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 27 avril 2026 à 20:06 (CEST) <!-- Message envoyé par User:UOzurumba (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30458046 --> == Actualités techniques n° 2026-19 == <section begin="technews-2026-W19"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/19|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * L’équipe chargée des fonctionnalités de [[mw:Special:MyLanguage/Article guidance|Guidage des articles]] invite les contributeurs et contributrices expérimentés des [[mw:Special:MyLanguage/Article guidance/Pilot wikis and collaborators|Wikipédia pilotes]] (arabe, bangla, japonais, portugais, persan, turc, anglais simplifié, espagnol et français) à contribuer à la traduction et à l’adaptation des [https://b24e11a4f1.catalyst.wmcloud.org/wiki/Category:Pages_using_article_guidance exemples de trames d’articles]. Ces trames guideront les contributeurs dans la création d’articles clairs, bien structurés et conformes aux règles lors de l’utilisation de [https://b24e11a4f1.catalyst.wmcloud.org/wiki/Special:NewArticle la fonctionnalité] dès son lancement en mai 2026. Des [[mw:Special:MyLanguage/Article guidance#Adapting a sample outline in a Wikipedia|instructions simples]] expliquant comment traduire et adapter ces trames sont disponibles. '''Actualités pour la contribution''' * Le [[:m:Special:MyLanguage/Product and Technology Advisory Council|Conseil consultatif sur les produits et les technologies]] a publié [[:m:Special:MyLanguage/Product and Technology Advisory Council/May 2026 draft PTAC recommendation for feedback|une proposition de recommandation]] d’une procédure type que les organisations affiliées à Wikimedia pourraient suivre pour contribuer au domaine technique. Les membres de la communauté sont invités à donner leur avis sur cette recommandation avant le 8 mai [[:m:Talk:Product and Technology Advisory Council/May 2026 draft PTAC recommendation for feedback|sur la page de discussion]]. * Le nombre de préférences de taille de la miniature disponibles dans MediaWiki va être réduit à trois options standardisées : ''petite'' (180 px), ''moyenne'' (250 px) et ''large'' (400 px), dans le cadre du travail en cours pour améliorer les performances et réduire la pression sur les services de miniatures. Par conséquent, les préférences existantes seront automatiquement adaptées à la nouvelle taille la plus proche (par exemple, les petites tailles comme 120 px ou 150 px s’afficheront à 180 px, tandis que les grandes tailles comme 300 px ou 360 px s’afficheront à 400 px). L’interface des préférences sera bientôt mise à jour pour refléter ces changements, et les utilisateurs qui souhaitent s’y opposer ou donner leur avis peuvent le faire. [https://phabricator.wikimedia.org/T424909] * Dorénavant, même lorsqu’une permission expire automatiquement, les utilisateurs recevront une notification Echo similaire à la notification normale pour les changements de permissions. Quant au [[m:Special:MyLanguage/Global reminder bot|robot global de rappel]], il continue de prévenir les utilisateurs une semaine ''avant'' que leurs droits ne soient sur le point d’expirer, afin qu’ils puissent les faire renouveler. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:32|la tâche soumise|les {{formatnum:32}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:32||s}} la semaine dernière]]. Par exemple, le problème du sélecteur de langue ULS dans [[m:Special:Translate|Special:Translate]] qui faisait défiler verticalement alors qu’il ne devait pas, a été résolu. Auparavant, lorsque les utilisateurs ouvraient le menu déroulant « Traduire en français » et commençaient à saisir le nom d’une langue, la boîte de dialogue défilait verticalement de quelques pixels même lorsqu'il y avait suffisamment d’espace pour afficher tous les résultats. Le menu déroulant ne se déplace plus inutilement lors du filtrage des langues. [https://phabricator.wikimedia.org/T358864] * La [[m:Special:GlobalWatchlist|liste de suivi globale]], qui vous permet de consulter vos listes de suivi provenant de plusieurs wikis sur une seule page, continue de s’améliorer. Par exemple, les listes de suivi pour les sites avec Wikibase tels que [[:d:|Wikidata]] prennent désormais en charge les éléments [[mw:Special:MyLanguage/Extension:EntitySchema|EntitySchema]] pour un meilleur suivi. Le mode Mises à jour en direct actualise désormais la page spéciale toutes les 60 secondes afin de se conformer aux [[mw:Special:MyLanguage/Wikimedia APIs/Rate limits|nouvelles limites globales d’accès à l’API]] pour une meilleure réactivité en temps réel. Par ailleurs, un bug de directionnalité du texte qui affichait les liens comme « changements 3 » au lieu de « 3 changements » dans les listes à directions mixtes a été corrigé. [https://phabricator.wikimedia.org/T415450][https://phabricator.wikimedia.org/T424422][https://phabricator.wikimedia.org/T418091] '''Actualités pour la contribution technique''' * La deuxième phase de [[mw:Special:MyLanguage/Wikimedia APIs/Rate limits|limitations globales d’accès à l’API]] a été déployée pour réduire l’[[diffblog:2026/03/26/quo-vadis-crawlers-progress-and-whats-next-on-safeguarding-our-infrastructure/|impact des robots IA]] et assurer un accès équitable et durable aux ressources de Wikimedia, en donnant la priorité au trafic humain et conforme à notre mission. Les [[mw:Special:MyLanguage/Wikimedia APIs/Rate limits#Limits|limites]] ne s’appliquent plus par heure mais par minute, produisant une meilleure répartition dans les structures de trafic ainsi qu’une meilleure prévisibilité de la charge de l’API. Les utilisateurs de la communauté ne devraient pas être affectés, et aucune action n’est requise. Les premières indications montrent que certains requérants basés sur l'agent utilisateur ajustent leur comportement, et environ 64 % du trafic API automatisé a été identifié. La surveillance continue, et Wikimedia Enterprise reste disponible pour l’assistance commerciale. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.46/wmf.27|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/19|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W19"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 4 mai 2026 à 22:43 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30498077 --> == Actualités techniques n° 2026-20 == <section begin="technews-2026-W20"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/20|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * La Communauté Technique a publié [[m:Special:MyLanguage/Community Wishlist/How to write a good wish|de nouvelles directives]] expliquant comment les souhaits sur la Liste de souhaits de la communauté sont triés et priorisés. La documentation vise à aider les contributeurs à rédiger des propositions plus solides en clarifiant les facteurs qui influencent les décisions de priorisation. Au-delà du nombre de votes, les directives mettent en avant des considérations telles que l'impact potentiel sur la communauté pour déterminer quels souhaits avanceront. '''Actualités pour la contribution''' * L'équipe de croissance des lecteurs lance une expérience pour tester une nouvelle [[mw:Special:MyLanguage/Readers/Reader_Growth/Share_Card|fonctionnalité de Partage de Carte]] qui permet aux lecteurs de créer des cartes visuellement attrayantes à partir d'articles Wikipédia ou de sections d'articles sélectionnées et de les partager en ligne, chaque carte renvoyant à l'article original afin d'aider à augmenter le lectorat et la découverte des articles. Le test A/B réservé aux mobiles ne sera disponible qu'à une partie des lecteurs sur les Wikipédia en arabe, chinois, français, vietnamien et anglais afin de mieux comprendre les habitudes de lecture et de partage, et est prévu pour commencer la semaine du 18 mai pour une durée de quatre semaines. * Les applications Wikipedia pour Android et iOS ont récemment publié en version bêta le [[mw:Special:MyLanguage/Wikimedia_Apps/Team/25th_Birthday_Reading_Challenge|défi de lecture de 25 jours]], dans le cadre des efforts visant à stimuler l'engagement des lecteurs en encourageant les utilisateurs à atteindre des objectifs de lecture. Pour suivre leur série de lectures pendant le défi, les utilisateurs de l'application peuvent ajouter un widget avec Baby Globe à leur écran d'accueil. Le défi commence officiellement le 11 mai. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:17|la tâche soumise|les {{formatnum:17}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:17||s}} la semaine dernière]]. Par exemple, un problème où la préférence globale pour activer la coloration syntaxique dans le wikitexte pouvait s'éteindre de manière inattendue après avoir été activée a maintenant été corrigé. [https://phabricator.wikimedia.org/T425286] '''Actualités pour la contribution technique''' * [[File:Octicons-tools.svg|12px|link=|alt=|Sujet technique]] Le module ResourceLoader <bdi lang="zxx" dir="ltr"><code><nowiki>mediawiki.ui.input</nowiki></code></bdi>, obsolète depuis [[m:Special:MyLanguage/Tech/News/2023/39|septembre 2023]], sera supprimé cette semaine. Il existe un [[mw:Special:MyLanguage/Codex/Migrating_from_MediaWiki_UI|guide pour migrer de l’interface MediaWiki UI vers Codex]] pour tous les outils qui l’utilisent. [https://phabricator.wikimedia.org/T420125] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.2|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/20|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W20"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 11 mai 2026 à 21:20 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30524429 --> == Actualités techniques n° 2026-21 == <section begin="technews-2026-W21"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/21|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * L'équipe de Wikipédia abstraite a identifié cinq wikis pilotes potentiels pour évaluer leur intérêt à adopter des articles abstraits sur leurs wikis. Les pilotes sont Wikipédia en Malayalam, en Bengali, en Dagbani, en Arabe et en Indonésien. La période de retour d'information sera ouverte jusqu'au 22 mai. Si votre communauté est intéressée à devenir un pilote, [[m:Talk:Abstract Wikipedia|faites-nous savoir sur Meta]]. '''Actualités pour la contribution''' * Une expérience visant à afficher [[mw:Special:MyLanguage/Readers/Reader Experience/Reading lists|les listes de lecture]] aux lecteurs non connectés sur le web mobile sera lancée le 18 mai sur les Wikipédias Allemande, Espagnole, Italienne, Portugaise, Polonaise, Néerlandaise, Turque et Ourdou, et durera un mois. Cet effort soutient des objectifs plus larges consistant à aider les lecteurs à enregistrer et organiser des articles pour une lecture ultérieure, tout en encourageant des habitudes qui pourraient mener à de futures contributions sur Wikipédia. * Pour prendre en charge un bouton de marquage dans la fonctionnalité bêta Liste de lecture, le menu "Outils > Action" a été mis à jour pour afficher des icônes, y compris l'indicateur en forme d'étoile de suivi qui aide les éditeurs à identifier les articles suivis temporairement. Les icônes correspondent désormais également à celles utilisées sur mobile, améliorant la cohérence entre les plateformes. Le changement est actuellement limité au menu des actions et concerne principalement les éditeurs ayant des droits d'utilisateur privilégié. [https://phabricator.wikimedia.org/T426008] * [[mw:Special:MyLanguage/VisualEditor/Suggestion Mode|Mode de Suggestion]] a été publié en tant qu'[[w:en:A/B test|test A/B]] pour les nouveaux éditeurs sur le site mobile à [[phab:T421189|~15 Wikipédias]]. L'expérience mesurera l'impact que le Mode de Suggestion a sur la proportion de sessions d'édition sur le web mobile par des nouveaux éditeurs qui aboutissent à des modifications constructives (non annulées) des articles. L'expérience évaluera également l'impact de la fonctionnalité sur la rétention des éditeurs et surveillera les changements dans les taux d'annulation et de blocage. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:27|la tâche soumise|les {{formatnum:27}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:27||s}} la semaine dernière]]. Par exemple, un problème dans l'application Android de Wikipédia où les images pourraient parfois ne pas se charger après avoir ouvert une notification de liste de lecture recommandée, a maintenant été corrigé. [https://phabricator.wikimedia.org/T418231] '''Actualités pour la contribution technique''' * L'[[mw:Special:MyLanguage/Wikidata Platform|équipe de la Plateforme Wikidata]] a publié sa [[d:Special:MyLanguage/Wikidata:SPARQL query service/WDQS backend update/Backend Replacement|recommandation de remplacement du backend]] et l'[[wikitech:Wikidata Query Service/WDQS Architecture re-design|architecture technique]] qui l'accompagne pour la migration du Wikidata Query Service (WDQS) hors de Blazegraph grap. Les retours sont attendus jusqu'au 25 mai 2026, en particulier sur les éventuelles lacunes et impacts sur les cas d'utilisation avancés. Les membres de la communauté Wikidata et les utilisateurs de WDQS sont également encouragés à aider à identifier les outils et flux de travail à fort impact qui pourraient nécessiter une attention sur [[d:Wikidata:SPARQL query service/WDQS backend update/High-Impact Use Cases|cette page]]. Les retours peuvent être partagés sur la [[d:Wikidata talk:SPARQL query service/WDQS backend update|page de discussion de la migration]] ou lors de la [[d:Special:MyLanguage/Wikidata:Blazegraph Migration Office Hours|prochaine heure de bureau]]. Voir le [[d:Special:MyLanguage/Wikidata:Wikidata Platform team/Newsletter|bulletin de l'équipe WDP]] pour plus de détails. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.3|MediaWiki]] '''En détails''' * Sur les Wikipédia en anglais, en français, en japonais et quelques autres, il y a eu un [[diffblog:2025/09/02/better-detecting-bots-and-replacing-our-captcha/|essai de hCaptcha]], un service tiers de détection de robots. L'essai a montré que hCaptcha détecte et dissuade efficacement certaines activités automatisées de mauvaise foi, à la fois par lui-même et en donnant des signaux aux [[w:en:Wikipedia:Village pump (technical)/Archive 225#Introducing SuggestedInvestigations|checkusers et stewards]] pour qu'ils enquêtent. Comme les résultats étaient positifs, hCaptcha sera déployé sur toutes les wikis au cours des prochaines semaines. [[mw:Special:MyLanguage/Product Safety and Integrity/Anti-abuse signals/hCaptcha|Voir la page du projet hCaptcha]] pour des informations techniques sur la mise en œuvre et les protections de la vie privée. [[diffblog:2026/05/04/better-detecting-bots-and-replacing-our-captcha-part-2/|En savoir plus]]. * La dernière mise à jour de la Technologie communautaire est désormais disponible, avec des progrès dans plusieurs initiatives de la Liste de souhaits communautaire, y compris l'extension des listes de lecture de l'application mobile au site web, la prise en charge de nouvelles langues pour "Who Wrote That" et le Tableau de bord personnel, des améliorations du rendu 3D et des graphiques, ainsi que des travaux à venir sur le tri des pages de discussion, la lecture audio et les flux de travail d'édition. La mise à jour partage également les priorités actuelles, les tendances de l'état de la Liste de souhaits et les opportunités de retour d'information de la communauté sur les domaines de concentration futurs et le Plan annuel 2026–2027 de la Wikimedia Foundation. [[m:Special:MyLanguage/Community Wishlist/Updates#May 13, 2026: Latest updates from the Community Tech team|Lisez le bulletin d'information complet pour plus de détails]]. '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/21|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W21"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 18 mai 2026 à 22:21 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30539262 --> == Actualités techniques n° 2026-22 == <section begin="technews-2026-W22"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/22|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * Faisant suite à une [[mw:Special:MyLanguage/Contributors/Account Creation Experiments#LOWM|expérience fructueuse sur la création de comptes]], un message d'avertissement pour les personnes déconnectées sera déployé sur les wikis Wikimédia durant la première semaine de juin. Ce changement n'affectera que les personnes déconnectées sur l'interface web mobile qui commencent à modifier. Cette nouvelle expérience est faite pour encourager la création de comptes, tout en autorisant aux utilisateurs de modifier à l'aide de comptes temporaires. Les résultats de l'expérience ont montré une augmentation de la création de compte d'environ 27 % pour ceux ayant vu le nouveau message. Comme prévu, puisque plus de personnes créent un compte, la création de comptes temporaires a diminué de 16 %. L'expérience n'a pas montré d'autres changements sur la qualité des modifications ou sur les autres indicateurs surveillés. [https://phabricator.wikimedia.org/T424595] '''Actualités pour la contribution''' * Pour des raisons de sécurité, les membres de certains groupes d’utilisateurs sont [[m:Special:MyLanguage/Mandatory two-factor authentication for users with some extended rights|forcés d'avoir l'authentification à 2 facteurs]] (A2F) d'activée. Les membres de ces groupes seront dans l'impossibilité de désactiver la dernière méthode d'A2F sur leur compte, et il sera impossible d'ajouter des utilisateurs sans A2F à ces groupes. Ces utilisateurs auront toujours la possibilité d'ajouter ou d'enlever des nouvelles méthodes d'authentification, tant qu'une de ces méthodes est toujours activée. Dans les prochaines semaines, les utilisateurs sans A2F seront retirés de ces groupes. Cela s'applique entre autres aux bureaucrates. Veuillez lire les tâches liées pour les dates de déploiement. [https://phabricator.wikimedia.org/T423119][https://phabricator.wikimedia.org/T423120] * L'[[m:Special:MyLanguage/WMDE Technical Wishes|équipe des souhaits techniques de Wikimédia Allemagne (WMDE)]] va lancer un [[w:fr:Test A/B|test A/B]] sur [[:phab:T415904|10 wikis]], pour essayer des [[m:WMDE Technical Wishes/References/Reference Previews|améliorations potentielles pour les aperçus de références]]. Cette expérience durera environ 2 semaines à la fin mai ou début juin et affectera 10 % du lectorat sur ordinateur sur les wikis participants. * Après deux expériences fructueuses, l'équipe Croissance du lectorat déploiera une fonctionnalité de [[mw:Special:MyLanguage/Readers/Reader Growth/Image Browsing|visionnage d'images]] en bêta pour toutes les Wikipédia sur mobile le 25 mai. Cela veut dire que toutes les personnes ayant les fonctionnalités bêtas activées verront cette fonctionnalité. Les autres pourront l’activer dans leurs préférences. Cette fonctionnalité inclura un carrousel de toutes les images d'un article en haut de celui-ci, avec la possibilité pour les contributeurs d’[[mw:Readers/Reader_Growth/Image_Browsing#Phase_2.1_beta_feature|exclure des images du carrousel d'un article ou d'enlever la fonctionnalité pour l'entièreté de l'article]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:30|la tâche soumise|les {{formatnum:30}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:30||s}} la semaine dernière]]. Par exemple, les fichiers STL tridimensionnels étaient affichés incorrectement par l'extension 3D du lecteur multimédia, ce qui est maintenant corrigé. [https://phabricator.wikimedia.org/T416723] '''Actualités pour la contribution technique''' * Les classes CSS dépréciées <bdi lang="zxx" dir="ltr"><code><nowiki>tleft</nowiki></code></bdi> et <bdi lang="zxx" dir="ltr"><code><nowiki>tright</nowiki></code></bdi> ont été remplacées par <bdi lang="zxx" dir="ltr"><code><nowiki>floatleft</nowiki></code></bdi> et <bdi lang="zxx" dir="ltr"><code><nowiki>floatright</nowiki></code></bdi> car les premières ne fonctionnent pas correctement sur toutes les plateformes, dont l'interface web mobile et l'application mobile. Les projets se servant de ces classes sont encouragés à vérifier leur usage et à planifier leur migration. Sachez que <bdi lang="zxx" dir="ltr"><code><nowiki>floatleft</nowiki></code></bdi> et <bdi lang="zxx" dir="ltr"><code><nowiki>floatright</nowiki></code></bdi> pourraient aussi être dépréciées dans le futur, même s’il n'y a pas de calendrier défini. [[phab:T426452|En savoir plus]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.4|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/22|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W22"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 25 mai 2026 à 23:52 (CEST) <!-- Message envoyé par User:Quiddity (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30584502 --> == Votez maintenant aux élections 2026 de l'U4C == <section begin="announcement-content" /> Les votants éligibles sont invités à participer à l'élection 2026 du [[m:Special:MyLanguage/Universal_Code_of_Conduct/Coordinating_Committee|Comité de coordination du Code de conduite universel]]. De plus amples informations – notamment sur la vérification de l'éligibilité, le processus de vote, les candidats et un lien vers le scrutin – sont disponibles sur Meta à la [[m:Special:MyLanguage/Universal_Code_of_Conduct/Coordinating_Committee/Election/2026|page d'informations sur les élections de 2026]]. Le scrutin se termine le 2 juin 2026 à [https://zonestamp.toolforge.org/1780358400 00 h 00 UTC]. Veuillez voter si votre compte est éligble. Les résultats seront disponibles avant le 14 juin 2026. -- en coopération avec l'U4C.<section end="announcement-content" /> [[m:User:Keegan (WMF)|Keegan (WMF)]] ([[m:User talk:Keegan (WMF)|talk]]) 27 mai 2026 à 19:14 (CEST) <!-- Message envoyé par User:Keegan (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Distribution_list/Global_message_delivery&oldid=30513860 --> == Actualités techniques n° 2026-23 == <section begin="technews-2026-W23"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/23|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * L'équipe [[mw:Special:MyLanguage/Readers/Reader Experience|Reader Experience]] mène une expérience pour montrer la fonctionnalité [[mw:Special:MyLanguage/Readers/Reader Experience/Reading lists|listes de lecture]], qui est encore en développement, aux lecteurs non connectés sur mobile afin de tester si elle encourage la création de compte à un rythme plus élevé que le bouton watchstar. L'[[mw:Special:MyLanguage/Readers/Reader Experience/Reading lists#Experiment timeline|expérience]] a été lancée le 18 mai sur les wikis en allemand, espagnol, italien, portugais, polonais, néerlandais, turc et ourdou, et elle durera un mois. * L'équipe Wikimedia Apps a publié la [[mw:Special:MyLanguage/Wikimedia Apps/Team/Explore Feed Refresh/Phase 1|Phase 1]] du flux d'accueil repensé pour l'application Android Beta. Le nouveau flux d'accueil comprend un onglet « Communauté » actualisé et un onglet « Pour vous » personnalisé contenant des recommandations de lecture mises à jour quotidiennement. La refonte fait partie d'un effort plus large visant à améliorer la découverte de contenu et à créer des expériences d'apprentissage plus engageantes dans les applications Wikipédia. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:18|la tâche soumise|les {{formatnum:18}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:18||s}} la semaine dernière]]. Par exemple, un problème où les images pouvaient ne pas se charger pour certaines modifications suggérées sur [[w:Special:Homepage|Special:Homepage]], laissant la vignette bloquée dans un état de chargement, a maintenant été corrigé. [https://phabricator.wikimedia.org/T424048] '''Actualités pour la contribution technique''' * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.5|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/23|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W23"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 1 juin 2026 à 23:08 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30613639 --> == Actualités techniques n° 2026-24 == <section begin="technews-2026-W24"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/24|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * Wikimedia Entreprise a relevé les limites d’utilisation gratuite de ses API. La limite mensuelle de requêtes pour l’API « à la demande » (<i lang="en">On-demand</i>) est passée de {{formatnum:5000}} à {{formatnum:50000}} requêtes, tandis que celle de l’API des instantanés (<i lang="en">Snapshot</i>) est passée de 15 à 30 requêtes par mois. De plus, les instantanés de contenus structurés sont désormais accessibles aux comptes gratuits. Ces changements élargissent l’accès aux données de Wikimedia Entreprise pour les développeurs et développeuses, les chercheurs et chercheuses et les organisations qui utilisent les contenus Wikimédia. [https://enterprise.wikimedia.com/blog/enhanced-free-api] '''Actualités pour la contribution''' * La [[mw:Special:MyLanguage/Wikimedia_Apps/Team/Explore Feed Refresh/Phase 1|nouvelle version du Fil d’exploration]], désormais appelé « Fil d’accueil », est en cours de déploiement auprès de 50 % des utilisateurs de l’application Wikipédia pour Android. Le fil d’accueil aide le lectorat à découvrir du contenu pertinent grâce à deux nouveaux onglets : « Communauté » et « Pour vous ». L’onglet « Communauté » propose un flux défilant de contenus sélectionnés et d’actualités provenant de l’ensemble de la communauté et du mouvement Wikimédia, tandis que l’onglet « Pour vous » offre une expérience en plein écran et par glissement qui présente des contenus adaptés aux centres d’intérêt de l’utilisateur ou utilisatrice. Cette refonte s’inscrit dans le cadre d’un travail en cours visant à améliorer la découverte et à enrichir l’expérience d’apprentissage au sein de l’application Wikipédia. * Le jeu-questionnaire quotidien [[mw:Special:MyLanguage/Wikimedia Apps/Team/iOS/"Which came first?" Game|Qu’est-ce qui est arrivé en premier ?]] est désormais disponible dans la version bêta de l’application Wikipédia pour iOS en anglais, allemand, français, portugais, russe, espagnol, arabe, chinois et turc. Le jeu s’appuie sur des événements historiques tirés de la rubrique « Éphéméride » de Wikipédia et met les lecteurs au défi de deviner lequel des deux événements s’est produit en premier. Le jeu avait déjà été lancé sur Android. Les communautés souhaitant rendre le jeu disponible dans leur langue peuvent [[mw:Special:MyLanguage/Wikimedia_Apps/Team/Games#Game availability by language|consulter les instructions et les conditions requises]]. * [[m:Special:MyLanguage/WMDE Technical Wishes/Sub-referencing|Les sous-références]], une nouvelle fonctionnalité de MediaWiki permettant aux contributeurs de réutiliser des références avec des détails différents, va commencer à être déployée sur les wikis Wikimédia après une phase pilote réussie. Le déploiement débutera le 8 juin pour la plupart des [[wikitech:Deployments/Train#Wednesday|wikis du groupe 1]] et Wikipédia en français, puis d'autres éditions linguistiques de Wikipédia bénéficieront de cette fonctionnalité au cours des prochains mois. Les communautés sont invitées à se préparer en vérifiant s’il existe des [https://translatewiki.net/w/i.php?title=Special%3ATranslate&group=ext-cite&language=en&action_source=search&filter=%21translated&optional=1&action=translate messages non traduits de l’extension Cite] dans leur langue et en passant en revue toute utilisation de l’outil [[mw:Special:MyLanguage/Reference Tooltips|Infobulles des références]], qui pourraient nécessiter des [[:phab:T416304#11668731|mises à jour]] pour prendre en charge la nouvelle fonctionnalité. Les wikis utilisant les [[mw:Special:MyLanguage/Help:Reference Previews|aperçus de référence]] n’ont aucune action à entreprendre. Les communautés peuvent également créer la [[Special:TrackingCategories|catégorie de suivi]] ''cite-tracking-category-ref-details'' en tant que catégorie cachée à l’aide de <code><nowiki>__HIDDENCAT__</nowiki></code> (ou d’un modèle dédié), et la relier à l’élément Wikidata correspondant [[d:Q129764848]]. [https://phabricator.wikimedia.org/T425662] * L'[[mw:Special:MyLanguage/Readers/Reader Growth/Mobile page previews#Experimentation|expérience d'Aperçus de page]] sur le Web mobile a pris fin. L'équipe a décidé de ne pas déployer cette fonctionnalité après que les résultats ont montré qu'elle n'avait pas d'impact statistiquement significatif sur la fidélisation des lecteurs, l'amélioration de la fidélisation étant le principal indicateur de réussite. Les « Aperçus de page », déjà disponibles sur ordinateur et dans les applications, affichent une vignette, le premier paragraphe et un lien vers l'article complet lorsque les lecteurs cliquent sur un lien bleu. L'expérience a testé cette fonctionnalité sur le Web mobile sur six versions de Wikipédia. * La [[mw:Special:MyLanguage/Codex/Design/Icons|bibliothèque d'icônes de l'interface utilisateur]] sera [[phab:T399175|mise à jour dans le courant de cette semaine ou la semaine prochaine]]. La plupart des quelque 300 icônes ont été légèrement peaufinées et une trentaine de nouvelles icônes ont été ajoutées. Ces modifications améliorent les icônes afin de les rendre plus cohérentes et plus compréhensibles, et d'offrir un meilleur équilibre visuel lorsqu'elles sont utilisées en groupe. * L'interface [[mw:Special:MyLanguage/Universal Language Selector|Sélecteur universel de langue]] (ULS) de MediaWiki, qui aide les utilisateurs à sélectionner du contenu dans d'autres langues, a été mise à jour. La nouvelle version améliore la rapidité et l'accessibilité, et les utilisateurs des projets Wikimédia peuvent désormais épingler des langues pour changer de langue plus rapidement. Le déploiement sur les sites Wikimédia se fera progressivement au cours des prochaines semaines. Vous pouvez la tester dès maintenant en tant que fonctionnalité bêta en sélectionnant [[Special:Preferences#mw-prefsection-betafeatures|les fonctionnalités bêta]] dans les préférences de votre profil et partager vos commentaires sur [[mw:Special:MyLanguage/Universal Language Selector/New ULS|la page du projet]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:21|la tâche soumise|les {{formatnum:21}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:21||s}} la semaine dernière]]. Par exemple, un problème du le tableau de bord d'analyse des pages vues sur pageviews.wmcloud.org qui a arrêté de mettre à jour les données graphiques en mai 2026, affectant tous les utilisateurs, a été résolu. [https://phabricator.wikimedia.org/T427171] '''Actualités pour la contribution technique''' * La signature de la fonction <bdi lang="zxx" dir="ltr"><code><nowiki>mw.util.addPortletLink()</nowiki></code></bdi> a été simplifiée. Les développeurs peuvent désormais passer un objet de configuration à la place d'une liste de paramètres positionnels lors de la création de liens vers des portlets. L'ancienne signature de la fonction reste prise en charge à des fins de compatibilité ascendante. Par exemple, au lieu de : <bdi lang="zxx" dir="ltr"><code><nowiki>mw.util.addPortletLink('p-cactions', '#', 'Stub', 'ca-stubtag', 'Add a stub tag to this page');</nowiki></code></bdi>, utilisez <bdi lang="zxx" dir="ltr"><code><nowiki>mw.util.addPortletLink('p-cactions', { href: '#', text: 'Stub', id: 'ca-stubtag', tooltip: 'Add a stub tag to this page' });</nowiki></code></bdi>. Les responsables de la maintenance des scripts sont invités à passer en revue les utilisations existantes de <bdi lang="zxx" dir="ltr"><code><nowiki>addPortletLink()</nowiki></code></bdi> et à les mettre à jour si nécessaire. Cette modification sera disponible sur tous les wikis à partir du 11 juin. Merci à Gerges, bénévole de la communauté, d'avoir apporté cette amélioration. [https://phabricator.wikimedia.org/T427945] * '''Discussion sur la liste de souhaits de la communauté''': les [[m:Special:MyLanguage/Community Wishlist/Updates#May 20, 2026: Community Tech becomes a program|changements introduits]] par les équipes Produit et Technologie visent à augmenter le nombre et la complexité des souhaits exaucés, notamment par la dissolution de l'équipe Community Tech. Ils [[m:Special:MyLanguage/Community Wishlist/Updates|mènent actuellement des discussions]] sur une [[m:Talk:Community Wishlist#Proposed direction for Wishlist|orientation proposée pour la liste de souhaits]] émanant des membres de la communauté. Cela inclut des moyens de structurer le vote annuel, un meilleur suivi des souhaits, la suppression de certains domaines prioritaires et des [[m:Special:MyLanguage/Community Wishlist/Updates|mises à jour concernant le personnel]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.6|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/24|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W24"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 8 juin 2026 à 23:29 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30650573 --> == Actualités techniques n° 2026-25 == <section begin="technews-2026-W25"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/25|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * L'[[mw:Special:MyLanguage/Readers/Reader Growth|équipe chargée de la croissance du lectorat]] a lancé une fonctionnalité bêta d'[[mw:Special:MyLanguage/Readers/Reader Growth/Image Browsing|exploration des images]] sur la version mobile de toutes les Wikipédias. Cette fonctionnalité affiche un carrousel d'images en haut des articles contenant au moins trois images. Les contributeurs peuvent configurer cette fonctionnalité à l'aide des commandes suivantes : pour masquer une image spécifique sur une page, utilisez soit <code>class=notpageimage</code> pour l'exclure des aperçus miniatures, soit <code>class=noviewer</code> pour l'exclure de MediaViewer. Le carrousel peut également être désactivé complètement sur une page à l'aide du mot magique <code><nowiki>__NOMEDIAVIEWERCAROUSEL__</nowiki></code>. Pour faire des retours ou signaler des bugs, rendez-vous sur la [[mw:Talk:Readers/Reader Growth/Image Browsing|page de discussion du projet]]. * Les [[mw:Special:MyLanguage/Help:Tables#class="wikitable"|Wikitables]] peuvent désormais être [[mw:Special:MyLanguage/Help:Sortable tables#Forcing the initial sort direction|triées par ordre décroissant]] dès le premier clic en ajoutant <code dir=ltr>data-sort-order="desc"</code> à la cellule d'en-tête. Auparavant, par défaut, cliquer une première fois sur l'en-tête d'une colonne entraînait un tri par ordre croissant. Cette nouveauté offre davantage de contrôle et de flexibilité pour les Wikitables, tandis que le comportement par défaut pour les clics suivants reste inchangé. [https://phabricator.wikimedia.org/T398416] '''Actualités pour la contribution''' * La fonctionnalité d'[[mw:Special:MyLanguage/Article guidance|Aide à la rédaction d'articles]] est actuellement en phase de test auprès de certains contributeurs qui créent de nouveaux articles sur les Wikipédias en anglais simplifié, en français et en turc. L'expérience débutera bientôt sur les Wikipédias en arabe et en bengali également. [[w:simple:Special:NewArticle|Cette fonctionnalité]] fournit aux contributeurs des conseils élaborés par la communauté afin de les aider à créer des articles conformes aux normes communautaires. Les contributeurs expérimentés peuvent continuer à créer ou à adapter des modèles pour des types d'articles spécifiques qui sont couramment créés par des contributeurs moins expérimentés. Ces modèles guident les contributeurs moins expérimentés dans la création d'articles de haute qualité. Un guide rapide des balises utilisées dans les modèles est disponible sur [[mw:Special:MyLanguage/Article guidance/Test feature guide#Markups in outlines|cette page]]. [[w:fr:Projet:Aide à la rédaction d'articles#Liste de plans d'aide à la rédaction|Des exemples de modèles]] pouvant être adaptés, ainsi que des instructions sur la manière de les adapter, se trouvent dans [[mw:Special:MyLanguage/Article guidance#Adapting a sample outline in a Wikipedia|cette section]] de la page du projet. * Les wikis qui souhaitent remplacer le bouton « indéfiniment » dans la page Special:Block pour les comptes temporaires (par exemple, les wikis qui bloquent les utilisateurs temporaires uniquement jusqu'à l'expiration de leur compte) pourront le faire en créant [[MediaWiki:ipb-indefinite-expiry-temporary-account]] avec la durée de blocage souhaitée. [https://phabricator.wikimedia.org/T427125] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:41|la tâche soumise|les {{formatnum:41}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:41||s}} la semaine dernière]]. '''Actualités pour la contribution technique''' * D'ici la fin du mois de juin, une chaîne « user-agent » valide sera requise pour les téléchargements automatisés de sauvegardes depuis le site dumps.wikimedia.org. Les requêtes automatisées fournissant une chaîne « user-agent » générique ou vide seront bloquées. Cette mesure [[phab:T400119|renforce l'application]] de la [[foundation:Special:MyLanguage/Policy:Wikimedia Foundation User-Agent Policy|politique relative à l'agent utilisateur]] en vigueur depuis longtemps. L'accès aux sauvegardes via Wikimedia Cloud Services restera inchangé. * La mise en place des [[mw:Wikimedia APIs/Rate limits|limites de débit des API]] à l'échelle mondiale est désormais achevée ; ces limites s'appliquent à toutes les API et sont fixées aux niveaux indiqués dans la documentation pour tous les groupes. Les bots fonctionnant sur Toolforge/WMCS ou disposant du droit d'utilisateur « bot » sur n'importe quel wiki restent exemptés. Tous les bots doivent continuer à respecter les bonnes pratiques décrites dans la documentation afin d'éviter d'être soumis à des limites de débit. * Le [https://api.wikimedia.org/wiki/Main_Page wiki du portail API] sera en lecture seule à partir de cette semaine (du 15 au 18 juin). La semaine suivante (du 22 au 25 juin), toutes les URL du wiki du portail API redirigeront vers [[mw:Wikimedia APIs|les API Wikimedia sur mediawiki.org]]. Pour en savoir plus, consultez la [[wikitech:API Portal/Deprecation|page du projet]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.7|MediaWiki]] '''Rencontres et évènements''' * Le 17 juin à 18 h (UTC), la WMF organisera une réunion sur Discord consacrée à la revue de code. L'[[mw:Special:MyLanguage/Developer Satisfaction Survey/2026|enquête sur la satisfaction des développeurs]] nous a permis de constater que les bénévoles rencontrent des difficultés avec la revue de code, et nous souhaitons discuter de ces expériences afin de trouver des solutions concrètes. Vous pouvez rejoindre la réunion [https://discord.gg/wikipedia?event=1514727511102062664 via le serveur Discord de la communauté Wikimedia]. * La [[m:Special:MyLanguage/Conferencia Wikimedia de América Latina 2026|Conférence Wikimedia d'Amérique latine]] organisera un hackathon régional qui réunira la communauté technique du mouvement Wikimedia, notamment des développeurs, des administrateurs système, des data scientists et des utilisateurs disposant de droits étendus. Les contributeurs techniques intéressés peuvent [https://docs.google.com/forms/d/e/1FAIpQLSf4osJzTHBJjQbYJk7TMVEJjTEQv7IgtsUDfP-o-qTgeRQQxw/viewform postuler à une bourse] pour y participer jusqu'au 21 juin à minuit (heure de la Bolivie, UTC-4). * Inscrivez-vous aux Wikimania Team Challenges pour participer à cet événement exceptionnel. Les défis par équipe se dérouleront en ligne et en présentiel les 21 et 22 juillet, avant la conférence Wikimania. Tout le monde est le bienvenu, quelles que soient ses compétences ou son inscription à Wikimania. Les équipes travailleront sur 10 défis importants visant à soutenir la communauté Wikimedia. Pour plus de détails, rendez-vous sur [[wmania:Special:MyLanguage/2026:Team challenges|la page des défis par équipe]] et [https://wikimedia.eventyay.com/wm/teamchallenges/ inscrivez-vous ici]. Les inscriptions se terminent le 20 juin à 23 h UTC. '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/25|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W25"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 15 juin 2026 à 18:48 (CEST) <!-- Message envoyé par User:UOzurumba (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30689604 --> == Actualités techniques n° 2026-26 == <section begin="technews-2026-W26"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/26|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * Les [[mw:Special:MyLanguage/Growth/Feature summary|fonctionnalités de croissance]] sont [[phab:T418115|désormais disponibles sur Wikidata]]. Cette mise à jour permet d'accéder au mentorat ([[mw:Special:MyLanguage/Help:Growth/Mentorship|s'il est configuré]]), au module Impact, au panneau d'aide et à une page d'accueil simplifiée pour les nouveaux arrivants (sans les suggestions de modifications). Les administrateurs de Wikidata continuent de paramétrer ces fonctionnalités via la configuration communautaire. '''Actualités pour la contribution''' * La page spéciale [[{{#special:RangeCalculator}}]] a été créée. Elle permet aux utilisateurs de trouver une plage d'adresses IP sans avoir à recourir à des outils externes. Jusqu'à présent, cet outil n'était accessible qu'aux CheckUsers. [https://phabricator.wikimedia.org/T268429] * Les [[m:Special:MyLanguage/WMDE Technical Wishes/Sub-referencing|sous-références]] sont une nouvelle fonctionnalité de MediaWiki qui permet aux contributeurs de réutiliser des références en modifiant certains détails. Elle sera déployée le 23 juin, sur la plupart des versions de Wikipédia de petite et moyenne taille. La [[m:Special:MyLanguage/WMDE Technical Wishes/Sub-referencing#deployment|FAQ]] répertorie les mesures à prendre sur votre wiki pour faciliter ce déploiement. Consultez le [[:phab:T414094|plan de déploiement]] pour connaître les prochaines étapes. [https://phabricator.wikimedia.org/T428902] * À partir de la semaine prochaine, les utilisateurs recevront une notification lorsqu'ils seront bloqués ou débloqués pour l'édition, ou si ce blocage venait à changer. [https://phabricator.wikimedia.org/T100974] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:32|la tâche soumise|les {{formatnum:32}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:32||s}} la semaine dernière]]. '''Actualités pour la contribution technique''' * À partir de la semaine prochaine, les filtres anti-abus configurés pour « exiger une vérification par CAPTCHA » s'appliqueront également aux utilisateurs disposant du droit <code>skipcaptcha</code>, ce qui inclut la plupart des utilisateurs auto-confirmés. Les bots en sont exemptés. Ce changement ne concerne que les modifications qui déclenchent un filtre anti-abus. Le droit <code>skipcaptcha</code> continuera à exempter les utilisateurs de l'obligation de résoudre des CAPTCHA dans le cadre d'une utilisation normale des wikis. [https://phabricator.wikimedia.org/T402595] * La documentation de référence relative à l'[[wikitech:Machine_Learning/LiftWing/API|API Lift Wing]] a été déplacée du portail API vers le [https://wikitech.wikimedia.org/w/index.php?api=lift-wing&title=Special%3ARestSandbox bac à sable REST] interactif. * Le wiki du Portail API est désormais fermé. Pour consulter la documentation relative aux API, rendez-vous sur [[mw:Special:MyLanguage/Wikimedia_APIs|Wikimedia APIs sur mediawiki.org]]. À compter du 22 juin, toutes les URL du wiki du Portail API (https://api.wikimedia.org/wiki/) redirigeront vers la page de mediawiki.org. [https://phabricator.wikimedia.org/T427537] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.8|MediaWiki]] '''Rencontres et évènements''' * Participez à une visioconférence le 25 juin à 14 h 30 UTC pour rencontrer les stagiaires actuels de Wikimédia participant au [[mw:Google_Summer_of_Code/2026|Google Summer of Code]] et à [[mw:Outreachy/Round_32|Outreachy]]. Les stagiaires présenteront leurs projets et feront une brève démonstration du travail qu'ils ont réalisé jusqu'à présent. Les participants sont invités à [[mw:event:Google_Summer_of_Code/Summer_2026_June_Internship_open_session|partager leurs idées et leurs contacts au sein de leur communauté]]. '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/26|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W26"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 23 juin 2026 à 15:05 (CEST) <!-- Message envoyé par User:Trizek (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30722494 --> == RFC about AI-generated content in Wikimedia Commons == <bdi lang="en" dir="ltr">Apologies for writing in English, please help translate this message to your language. You are invited to participate in a [[c:Commons:Requests for comment/Policy update for AI content|request for comment on Wikimedia Commons about a policy update for AI content]]. This may affect files that are uploaded to Wikimedia Commons for use on this project. Thank you. [[m:User:Codename Noreste|Codename Noreste]] ([[m:User talk:Codename Noreste|discussion]])</bdi> 23 juin 2026 à 19:11 (CEST) <!-- Message envoyé par User:Codename Noreste@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Distribution_list/Global_message_delivery&oldid=30513860 --> == Intégration du lien vers les contacts juridiques et de sécurité dans le pied de page de votre wiki == <section begin="Message"/> '''Contacts juridiques et de sécurité''' Bonjour à toute la communauté, la Fondation Wikimedia a mis à disposition une [[wmf:Special:MyLanguage/Legal:Wikimedia Foundation Legal and Safety Contact Information|page unique dédiée aux mentions légales et à la sécurité]], à ajouter en pied de page de votre wiki, afin de garantir l'accès à des informations juridiques exactes. Il s'agit d'une exigence réglementaire. Nous avons déjà mis en place des liens vers les wikis en anglais, allemand, italien, espagnol et d'autres langues de Wikipedia, et nous les déploierons bientôt sur votre wiki. Pour en savoir plus, [[m:Special:MyLanguage/Wikimedia_Foundation_Legal_and_Safety_Contacts_FAQ|consultez la page du projet]] et n'hésitez pas à laisser vos commentaires dans ce fil de discussion ou sur la [[m:Special:MyLanguage/Talk:Wikimedia Foundation Legal and Safety Contacts FAQ|page de discussion]]. <section end="Message"/> -- [[User:Sannita (WMF)|User:Sannita (WMF)]] ([[User talk:Sannita (WMF)|talk]]) 25 juin 2026 à 15:30 (CEST) <!-- Message envoyé par User:Sannita (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=User:Sannita_(WMF)/Mass_sending_test&oldid=30731267 --> == Actualités techniques n° 2026-27 == <section begin="technews-2026-W27"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/27|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * Dans le cadre des [[mw:Special:MyLanguage/Contributors/Account Creation Experiments|expériences sur la création de comptes]], l'équipe Growth a testé l'ajout d'une icône de compte utilisateur dans l'en-tête du site mobile pour les utilisateurs non connectés, offrant ainsi un accès direct aux actions « Créer un compte » et « Se connecter ». Cette expérience a permis d'augmenter le nombre de créations de comptes d'environ 20 % sans nuire à la qualité des modifications ni au taux de modifications constructives. Cette fonctionnalité sera désormais déployée sur tous les wikis de la Fondation Wikimédia sur le site mobile au cours de la première semaine de juillet. [https://phabricator.wikimedia.org/T428220] * À la suite d'une [[phab:T426248|expérience concluante]], les utilisateurs connectés qui n'ont pas [[mw:Special:MyLanguage/Help:Email_confirmation|confirmé leur adresse e-mail]] lors de la création de leur compte voient s'afficher une nouvelle bannière leur demandant de finaliser cette procédure. Cela permet de réduire le risque que les utilisateurs se retrouvent bloqués hors de leur compte et rend les adresses e-mail associées aux comptes globalement plus fiables. Cette mesure s'inscrit dans le cadre du projet [[mw:Special:MyLanguage/Product Safety and Integrity/Account Security|Sécurité des comptes]]. [https://phabricator.wikimedia.org/T428292] * Une mise à jour de [[Special:Search|Recherche]] affine le comportement de <bdi lang="zxx" dir="ltr"><code><nowiki>-prefix:</nowiki></code></bdi> lorsqu'il est utilisé pour exclure des résultats. Auparavant, l'utilisation de <bdi lang="zxx" dir="ltr"><code><nowiki>-prefix:</nowiki></code></bdi> avec la négation pouvait élargir involontairement les résultats de recherche en ajoutant les espaces de noms inclus dans le champ de recherche, ce qui entraînait un comportement déroutant pour les utilisateurs s'attendant à un filtre d'exclusion simple. Avec cette mise à jour, <bdi lang="zxx" dir="ltr"><code><nowiki>-prefix:</nowiki></code></bdi> exclura désormais strictement les titres de pages correspondants comme prévu et pourra afficher un avertissement si l'espace de noms concerné n'a pas été explicitement sélectionné. Le comportement de <bdi lang="zxx" dir="ltr"><code><nowiki>prefix:</nowiki></code></bdi> sans négation reste toutefois inchangé. [https://phabricator.wikimedia.org/T427443] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:33|la tâche soumise|les {{formatnum:33}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:33||s}} la semaine dernière]]. Par exemple, le problème qui empêchait les réviseurs utilisant la barre d'outils « Page Curation » d'être automatiquement abonnés aux discussions qu'ils avaient lancées sur les pages de discussion a désormais été résolu. Les réviseurs recevront désormais des notifications lorsqu'une personne répondra à ces discussions. [https://phabricator.wikimedia.org/T329346] '''Actualités pour la contribution technique''' * À compter du 29 juin, les téléchargements automatisés depuis le site web « dumps.wikimedia.org » seront soumis à la [[Foundation:Special:MyLanguage/Policy:Wikimedia Foundation User-Agent Policy|politique relative aux user-agents]]. Les requêtes automatisées utilisant un user-agent générique ou vide seront bloquées. L'accès aux sauvegardes via Wikimedia Cloud Services n'est pas affecté. Cette mesure fait suite à l'annonce publiée dans le [[m:Special:MyLanguage/Tech/News/2026/25|numéro 2026/25 de Tech News]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.9|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/27|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W27"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 29 juin 2026 à 13:48 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30744833 --> == Actualités techniques n° 2026-28 == <section begin="technews-2026-W28"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/28|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:34|la tâche soumise|les {{formatnum:34}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:34||s}} la semaine dernière]]. Par exemple, les résultats de la barre de recherche sur Wikidata étaient en anglais au lieu d’utiliser la bonne langue de repli pour les utilisateurs de variantes de langues. Ce problème a maintenant été corrigé : les suggestions de recherche suivront désormais correctement la chaîne des langues de repli. [https://phabricator.wikimedia.org/T429769] '''Actualités pour la contribution technique''' * En préparation de la [[m:Special:MyLanguage/Event:Celebrate Women|campagne Célébrons les femmes]] prévues pour mars 2027, l’[[m:Special:MyLanguage/Wikimedia Foundation/Advancement/Community Growth/Content Enablement|équipe Activation du contenu]] de Wikimedia Foundation a lancé un sondage composé de 22 questions pour mieux comprendre les contributions techniques des personnes s’identifiant comme femmes sur les projets Wikimedia. Répondre au sondage prend environ 15 à 20 minutes ; il restera ouvert jusqu’au 20 juillet 2026. Les [[m:Special:MyLanguage/Celebrate Women/Technical contributions survey|questions]] sont aussi sur wiki pour les regarder auparavant. * L’[[mw:Special:MyLanguage/Extension:Score|extension Partitions]] prend désormais en charge le rendu de partition musicales comme images SVG, en plus du format PNG. Cela répond à [[:phab:T49578|une vieille demande]] et résout des problèmes anciens de qualité de l’image. Les deux formats sont désormais fournis aux clients web : PNG dans l’attribut <bdi lang="zxx" dir="ltr"><code><nowiki>src</nowiki></code></bdi> et SVG dans l’attribut <bdi lang="zxx" dir="ltr"><code><nowiki>srcset</nowiki></code></bdi>. * Le nouvel analyseur syntaxique [[wikitech:Parsoid|Parsoid]] continue [[mw:Special:MyLanguage/Parsoid/Parser_Unification/Updates|d’être déployé sur d’autres wikis]], ce qui rend plus facile l’introduction de nouvelles fonctionnalités de lecture et de modification. Il a été activé sur Wikipédia en français, amenant la couverture totale à 78,9 % des pages vues de Wikipédia. Le déploiement sur Wikipédia en anglais sur ordinateur va progresser cette semaine. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.10|MediaWiki]] '''En détails''' * La [[diffblog:2026/06/29/wikimedia-hackathon-2026-building-collaborating-and-shaping-the-future-together/|publication sur le blog récapitulant]] le Hackathon Wikimedia 2026 est désormais en ligne. Il met en avant les projets, séances et activités sociales de l’évènement de cette année et présente les premières grandes lignes du Hackathon Wikimedia 2027. '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/28|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W28"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 6 juillet 2026 à 15:56 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30773578 --> == Actualités techniques n° 2026-29 == <section begin="technews-2026-W29"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/29|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * [[mw:Special:MyLanguage/Growth/Revise_Tone|Revise Tone]] aide les nouveaux contributeurs à identifier les passages des articles de Wikipédia susceptibles de contenir un langage non encyclopédique et les encourage à envisager d'en réviser le ton. Cette fonctionnalité a fait l'objet d'un [[w:en:A/B_testing|test A/B]] sur les Wikipédias en arabe, en anglais, en français et en portugais, où le taux d'achèvement des tâches par les nouveaux contributeurs [[mw:Special:MyLanguage/Growth/Revise_Tone#Experiment_Results|a augmenté de 38,7 %]] par rapport à la tâche de relecture par défaut, sans que la qualité des modifications n'en soit affectée. Le test s'est achevé le 9 juillet et la fonctionnalité est désormais accessible à tous sur ces wikis ; elle est configurable via la configuration communautaire. [[phab:T426364|Il est prévu]] de déployer « Revise Tone » sur d’autres wikis. * La configuration communautaire permettant la [[mw:Special:MyLanguage/Help:Growth/Mentorship#Automated mentor list cleanup|suppression automatique des mentors inactifs]] selon des critères configurables sera activée le jeudi 16, [[mw:Special:MyLanguage/Growth/Deployment|sur certains wikis]], afin de maintenir à jour les listes de mentors. Les mentors sont des contributeurs expérimentés qui choisissent d'aider les nouveaux utilisateurs sur le wiki grâce aux [[mw:Special:MyLanguage/Growth/Feature summary|fonctionnalités de croissance]]. Les administrateurs peuvent désormais préparer les paramètres via [[w:Special:CommunityConfiguration/Mentorship|Special:CommunityConfiguration/Mentorship]] ; ces fonctionnalités prendront effet à partir de jeudi. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:38|la tâche soumise|les {{formatnum:38}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:38||s}} la semaine dernière]]. Par exemple, un problème où certains utilisateurs de l'application Android Wikipedia ont été déconnectés immédiatement après leur connexion, les empêchant de rester connectés et de modifier les pages, a maintenant été résolu. [https://phabricator.wikimedia.org/T316916] '''Actualités pour la contribution technique''' * La modification d'une page à l'aide de scripts utilisateur ou de gadgets entraînait la réinitialisation des étiquettes de liste de suivi que l'utilisateur avait attribuées à cette page. Ce problème a désormais été résolu. [https://phabricator.wikimedia.org/T423778] * Pour contourner un bug de Safari (voir [[phab:T425211]]), sur les wikis utilisant Parsoid, wikilink hrefs utilise désormais des URL absolues au lieu d'URL relatives au protocole. La sortie de l'API REST reste inchangée et continue d'utiliser des URL relatives au protocole. Les gadgets, les scripts utilisateur, les bots et les feuilles de style CSS devront probablement être adaptés s'ils s'appuyaient sur la présence d'URL relatives au protocole dans wikilink hrefs. [https://phabricator.wikimedia.org/T431358] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.11|MediaWiki]] '''En détails''' * L'équipe chargée de la plateforme d'expérimentation de la Fondation Wikimedia a publié un article de blog faisant le bilan de sa première année d'expérimentation structurée. Cet article met en avant des expériences couronnées de succès, telles que « Paste Check », « Reference Check » et « Tone Check », qui ont permis d'améliorer les résultats des modifications et ont été étendues à un plus grand nombre d'utilisateurs, ainsi que des expériences qui n'ont pas abouti à des changements au niveau du produit. [[diffblog:2026/07/07/moving-the-needle-how-we-test-new-ideas-across-wikimedia-projects|En savoir plus]]. '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/29|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W29"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 13 juillet 2026 à 18:11 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30804401 --> == Actualités techniques n° 2026-30 == <section begin="technews-2026-W30"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/30|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * L'[[mw:Special:MyLanguage/Reader/Reader Experience|équipe chargée de l'expérience utilisateur]] a pris en compte les commentaires de la communauté concernant l'emplacement des boutons « Watchstar » et « Watchlist » pour la [[mw:Special:MyLanguage/Readers/Reader Experience/WE3.3.4 Reading lists|fonctionnalité bêta « Listes de lecture »]], qui permettrait d'enregistrer des articles pour les lire plus tard – un [[m:Special:MyLanguage/Community Wishlist/W102|élément de la liste de souhaits]] visant à intégrer cette fonctionnalité sur le site web. Les contributeurs sont invités à activer cette fonctionnalité bêta pour la tester et [[phab:T426453|partager leurs impressions]]. * Le [[mw:Special:MyLanguage/VisualEditor/Suggestion Mode|mode Suggestions]] propose des suggestions d'édition au sein de l'éditeur visuel afin d'améliorer les articles de Wikipédia. [[Special:EditChecks|Toutes les suggestions]] sont [[mw:Special:MyLanguage/Help:Suggestion mode#For administrators – local customization|configurables par la communauté]]. La fonctionnalité [[mw:Special:MyLanguage/Edit check/TextMatch|TextMatch]] permet aux bénévoles de créer des suggestions locales personnalisées. Cette fonctionnalité recherche des chaînes de texte dans les articles et prend désormais en charge les expressions régulières. Cela offre aux bénévoles davantage de précision et de flexibilité quant aux types de suggestions locales qu’ils peuvent créer. Remarque : les suggestions [[mw:Special:MyLanguage/Edit check/Configuration#:~:text=maximumEditcount|peuvent être ciblées]] en fonction du nombre de modifications effectuées par la personne qui édite, ainsi que d’autres aspects de la page. Vous pouvez [[mw:Special:MyLanguage/Help:Suggestion mode#Create custom local types of Suggestions|trouver des exemples provenant d’autres communautés]] pour vous inspirer, notamment des TextMatches qui détectent : les fautes de frappe, les erreurs grammaticales, les publicités potentielles, les clichés, l’utilisation incorrecte des tirets ou des traits d’union, les mots-clés temporels non spécifiques, les noms obsolètes, et bien plus encore. Tout [[mw:Special:MyLanguage/Talk:VisualEditor/Suggestion Mode|retour]] est le bienvenu. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:27|la tâche soumise|les {{formatnum:27}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:27||s}} la semaine dernière]]. Par exemple, un problème lié à l'outil SVG Translate, qui pouvait utiliser une version obsolète d'un fichier, entraînant ainsi le remplacement des traductions existantes lors du téléchargement de nouvelles traductions, a désormais [[phab:T430577|été résolu]]. Au total, au cours du dernier trimestre, d'avril à juin 2026, environ 337 tâches communautaires ont été résolues par la Fondation Wikimedia. '''Actualités pour la contribution technique''' * Sur les wikis utilisant Parsoid, ce dernier affiche désormais un maximum de 1 250 images par page. Une nouvelle catégorie de suivi, « media-limit-reached », sera bientôt disponible pour identifier les pages où cette limite est atteinte, ce qui facilitera la recherche des contenus dont l'affichage des médias a pu être restreint lors du rendu. Consultez [[phab:T430854]] pour plus d'informations et pour nous faire part de vos commentaires. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.12|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/30|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W30"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 21 juillet 2026 à 07:46 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30836091 --> == Request for comment (the future of Abstract Wikipedia) == <bdi lang="en" dir="ltr" class="mw-content-ltr"> Apologies if this has not yet been translated into your wiki's language. {{int:Please-translate}} You are invited to voice your opinions in a [[:m:Requests for comment/The future of Abstract Wikipedia|request for comment about the future of Abstract Wikipedia]]. {{Int:Feedback-thanks-title}} [[:m:User:Kowal2701|Kowal2701]] ([[:m:User talk:Kowal2701|talk]]) 24 juillet 2026 à 14:24 (CEST) </bdi> <!-- Message envoyé par User:DreamRimmer@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Distribution_list/Global_message_delivery&oldid=30513860 --> == <span lang="en" dir="ltr">Tech News: 2026-31</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W31"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/31|Translations]] are available. '''Updates for editors''' * [[File:Maki-gift-15.svg|12px|link=|class=skin-invert|Wishlist item]] [[mw:Special:MyLanguage/ContentTranslation|Content Translation]] now supports dark mode, fulfilling a [[m:Community Wishlist/W544|Community Wishlist request]]. This brings the tool in line with the accessibility features available in the Vector 2022 and Minerva skins, helping reduce visual fatigue for users translating content. [https://phabricator.wikimedia.org/T367077] * DiscussionTools' source mode and the 2017 wikitext editor will now offer autocomplete for links (<bdi lang="zxx" dir="ltr"><code><nowiki>[[</nowiki></code></bdi>), templates (<bdi lang="zxx" dir="ltr"><code><nowiki>{{</nowiki></code></bdi>), HTML and parser tags (<bdi lang="zxx" dir="ltr"><code><nowiki><</nowiki></code></bdi>), and magic words (<bdi lang="zxx" dir="ltr"><code><nowiki>__</nowiki></code></bdi>), making it quicker and easier to insert links, templates, and other wiki markup while editing. [https://phabricator.wikimedia.org/T432400] * The [[mw:Special:MyLanguage/Readers/Reader Growth/Mobile page previews|Readers Growth team]] has concluded its experiment with mobile page previews and will not roll out the feature. Page Previews are a pop-up bottom sheet that appears when readers tap a blue link, showing a thumbnail, lead paragraph, and an option to open the article. The experiment showed flat retention and negative indicator metrics, suggesting that mobile web readers preferred navigating directly to linked articles rather than using page previews. * The Reader Experience team has seen encouraging early results from the [[mw:Special:MyLanguage/Readers/Reader Experience/Reading lists|Reading Lists feature]], with 93% of participating users reporting that it was useful. Reading Lists help active readers save articles for future reading and support their learning goals on Wikimedia projects. The team plans further improvements before expanding the feature to more users. * The [[mw:Special:MyLanguage/Wikimedia Apps/Team/Explore Feed Refresh|Explore Feed Refresh]] initiative was tested with new and casual Wikipedia app readers. The refreshed feed helps readers discover new and relevant content. After a 10.5% increase in engagement with the feed, Wikimedia Apps team has decided to scale the Home Feed redesign to iOS with the learnings from the Android release applied. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:23}} community-submitted {{PLURAL:23|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, an issue where subject names in the Article Guidance feature were displayed with incorrect capitalization on French Wikipedia, has now been fixed. Subject names will now follow the correct capitalization rules for the language. [https://phabricator.wikimedia.org/T427201] '''Updates for technical contributors''' * After running several [[mw:Special:MyLanguage/Contributors/Account Creation Experiments|Account Creation Experiments]] to improve registration completion rates, a new version of the username field on [[Special:CreateAccount|Create Account]] has been rolled out. It includes [[:c:File:Create account - July 2026 updates.png|a popover summarizing the username policy]] to provide clearer guidance during account creation. As part of this change, the messages <bdi lang="zxx" dir="ltr"><code><nowiki>createacct-helpusername</nowiki></code></bdi> and <bdi lang="zxx" dir="ltr"><code><nowiki>createacct-username-help</nowiki></code></bdi> that several communities have configured will no longer be used. If communities want to customize the guidance shown in the new popover, they can instead edit the following messages: <bdi lang="zxx" dir="ltr"><code><nowiki>createacct-username-policy-popover-bullet1</nowiki></code></bdi>, <bdi lang="zxx" dir="ltr"><code><nowiki>createacct-username-policy-popover-bullet2</nowiki></code></bdi>, and <bdi lang="zxx" dir="ltr"><code><nowiki>createacct-username-policy-popover-bullet3</nowiki></code></bdi>. [https://phabricator.wikimedia.org/T430604] * Later this week, the [[mw:Special:MyLanguage/Help:Extension:CodeMirror|CodeMirror syntax highlighter]] will offer [[w:en:Theme (computing)|themes]]. The themes can be picked from a dropdown menu in the full [[mw:Special:MyLanguage/Help:Extension:CodeMirror#CodeMirror preferences|CodeMirror preferences]] dialog. For wikitext, available themes are default, colorblind-friendly (previously the colorblind preference option on [[Special:Preferences#mw-prefsection-editing]]) and no-highlighting. For code languages (i.e., CSS/JavaScript/JSON/Vue/Lua), there are several themes available. These same themes will eventually be available for wikitext, too. [https://phabricator.wikimedia.org/T163533] * From now on, wikis can restrict editing in the "User" namespace to only the page owner and certain user groups. [[mw:Special:MyLanguage/Manual:$wgRestrictUserPageEditing|Read the configuration documentation]] to learn more. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.47/wmf.14|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/31|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W31"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 27 juillet 2026 à 20:49 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30856308 --> == Actualités techniques n° 2026-32 == <section begin="technews-2026-W32"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/32|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * L’[[mw:Special:MyLanguage/Readers/Reader Experience|équipe Expérience de lecture]] a développé une [https://82db7c8d4b.catalyst.wmcloud.org/w/index.php?title=Regent%27s_Park&uselang=de démonstration d’un correctif] qui entoure la barre d’outils sur deux lignes quand il n’y a pas assez d’espace horizontalement pour tous les boutons. Le but est de réduire l’encombrement de la barre d’outil de Vector 2022 qui se constate parfois sur Wikipédia dans certaines langues avec certaines tailles d’écran. [https://phabricator.wikimedia.org/T429518] * L’équipe Expérience de lecture prévoit de lancer les [[mw:Special:MyLanguage/Readers/Reader Experience/Reading lists|Listes de lecture]], un [[m:Special:MyLanguage/Community Wishlist Survey 2021/Mobile and apps/Have Apps reading lists available on Destop/Mobile|souhait de la communauté]] que vous pouvez déjà essayer en bêta, à partir de septembre. Mais avant cela, l’aide des traducteurs et traductrices est nécessaire pour [https://translatewiki.net/w/i.php?title=Special%3ATranslate&group=ext-readinglists&filter=&action=translate plusieurs messages] à traduire dans un maximum de langues. Cette fonctionnalité appuie les objectifs de lecture et d’apprentissage sur Wikipédia. * La semaine prochaine, le sommaire sur les pages des fichiers de Wikimedia Commons sera amélioré en fusionnant le sommaire spécifique aux pages de fichier avec le sommaire classique des pages. Cela rendra plus facile la compréhension de la structure des pages de fichier, la navigation vers une section spécifique et le partage de liens vers une section en particulier. [https://phabricator.wikimedia.org/T332644] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:24|la tâche soumise|les {{formatnum:24}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:24||s}} la semaine dernière]]. Par exemple, certaines images [[w:fr:Tagged Image File Format|TIFF]] ne s’affichaient pas après avoir cliqué sur leur vignette : une image cassé s’affichait au lieu de l’image en taille réelle. Ce problème est désormais corrigé. [https://phabricator.wikimedia.org/T429326] '''Actualités pour la contribution technique''' * Le sélecteur de variables et de fonctions dans les Filtres anti-abus a été mis à jour pour prendre en charge la recherche et l’autocomplétion. Il permettra désormais aux mainteneurs de filtres de retrouver la variable ou fonction désirée plus rapidement. [https://phabricator.wikimedia.org/T323698] * Les formats MJPEG et VP8 sont retirés du lecteur vidéo. Le format MP4 (MPEG-4 Partie 2) est ajouté à la place : il fournit une meilleure qualité pour les vieux modèles d’iPhone. Plusieurs semaines seront peut-être nécessaires pour mettre à jour toutes les vidéos déjà en ligne. Le format par défaut pour les appareils modernes ne change pas (VP9/WebM). [https://phabricator.wikimedia.org/T358266] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.15|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/32|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W32"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 3 août 2026 à 21:46 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30872536 --> == Actualités techniques n° 2026-33 == <section begin="technews-2026-W33"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/33|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * [[File:Maki-gift-15.svg|12px|link=|class=skin-invert|Concerne un souhait]] Un nouvel assistant à diagrammes (''ChartWizard'') est [[c:Special:ChartWizard/Data:Example.Pie.chart|désormais disponible sur Wikimedia Commons]] pour les utilisateurs et utilisatrices qui souhaitent créer des graphiques à partir de leurs propres données. Cet assistant rend l’[[mw:Special:MyLanguage/Extension:Chart|extension Chart]] plus accessible aux débutants en permettant la création de diagrammes (par exemple des histogrammes ou des graphiques en secteurs) sans devoir utiliser du JSON. Les utilisateurs peuvent encore repasser à l’éditeur JSON s’ils le préfèrent. Vos retours sur ce nouvel outil sont les bienvenus sur la [[m:Talk:Community Wishlist/W414|page de discussion du souhait]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:19|la tâche soumise|les {{formatnum:19}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:19||s}} la semaine dernière]]. Par exemple, le gadget Photo du jour de l’appli Wikipédia pour iOS affichait par erreur la même image chaque jour au lieu d’en afficher une nouvelle quotidiennement. Cela a désormais été corrigé. [https://phabricator.wikimedia.org/T430692] '''Actualités pour la contribution technique''' * Les images SVG de [[mw:Special:MyLanguage/Extension:Math|formules mathématiques]] seront bientôt générées par le navigateur plutôt que côté serveur. MathML continue d’être généré côté serveur et est rendu dans le navigateur sans JavaScript. Ce changement aura lieu sur Wikibooks le 12 aout, sur Wikisource le 19 aout et sur Wikipédia du 20 au 27 aout. Vous pouvez l’essayer en choisissant « {{int:Mw-math-mathjax}} » dans vos préférences. Cela fait partie de la [[mw:Special:MyLanguage/RESTBase/deprecation|dépréciation de RESTBase]] et [[phab:T431372|de celle de Mathoid]]. [https://phabricator.wikimedia.org/T271001] * <span class="mw-translate-fuzzy">Les pages de catégorie permettront bientôt de trier les pages de la catégorie par ordre chronologique d’ajout dans la catégorie. Cela permettra de retrouver facilement les pages récemment ajoutées ou celles depuis longtemps dans la catégorie. Cela améliorera aussi le traitement des catégories de maintenance, par exemple les demandes de suppression ou d’autres tâches nécessitant un traitement chronologique. Utilisez l’argument <bdi lang="zxx" dir="ltr"><code><nowiki>cldsort=timestamp</nowiki></code></bdi> dans l’URL de la vue d’une catégorie pour trier la liste de pages.</span> [https://phabricator.wikimedia.org/T433768] * Les [[mw:Special:MyLanguage/Extension:Gadgets|gadgets]] et scripts utilisateur sur les wikis Wikimedia peuvent désormais utiliser les [[phab:T395347|fonctionnalités de ES 2018]] et [[phab:T419142|celles de ES 2019]] dans le code JavaScript. Auparavant, la plateforme n’acceptait qu’ES 2017. MediaWiki valide le code source pour protéger les fonctionnalités d’erreurs de syntaxes et pour vérifier que les scripts sont fonctionnels dans tous [[mw:Special:MyLanguage/Compatibility#Browser_support_matrix|les navigateurs pris en charges]]. [https://phabricator.wikimedia.org/T419142] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.16|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/33|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W33"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 10 août 2026 à 22:45 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30901051 --> == <span lang="en" dir="ltr">Tech News: 2026-34</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W34"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/34|Translations]] are available. '''Weekly highlight''' * The [[mw:Special:MyLanguage/Help:Extension:CampaignEvents/Registration/Worklist|Worklist feature]] for the Event Registration tool is now live on all Wikimedia wikis. With Worklist, event organizers can add the articles their event will focus on directly to the event page. The Worklist also powers [[mw:Special:MyLanguage/Help:Extension:CampaignEvents/Registration/Worklist#How Event Pathways uses the worklist|Event Pathways]] which notifies other editors of the upcoming or ongoing event when they edit an article featured in the event's Worklist. This is the minimum viable version (MVP), and feedback is welcome. Organizers are encouraged to try the feature. A hands-on [[m:Special:MyLanguage/Event:Worklist Setup Workshop: Get Your Event Ready|Worklist Setup Workshop]] will take place on 18 August at 16:00 UTC and 19 August at 11:00 UTC. '''Updates for editors''' * [[Special:ShortPages]] displays short pages by their size, but in many cases it gets filled with disambiguations and soft redirects, making it harder to find the short articles themselves. Starting this weekend, you will be able to choose not to include an article in the special page by adding the magic word <bdi lang="zxx" dir="ltr"><code><nowiki>__EXPECTSHORTPAGE__</nowiki></code></bdi>. [https://phabricator.wikimedia.org/T433203] * One new wiki has been created: a {{int:project-localized-name-group-wikipedia/en}} in [[d:Q3436680|Bole]] ([[w:bol:|<bdi lang="zxx" dir="ltr"><code><nowiki>w:bol:</nowiki></code></bdi>]]) [https://phabricator.wikimedia.org/T429921] * Starting the week of August 17, the page toolbar will wrap onto two lines when there is not enough horizontal space for all the buttons. This is a [[phab:T429518|fully merged patch from the Reader Experience team]] which aims to reduce crowding in the Vector 2022 toolbar, that may occur on some language Wikipedias at certain screen widths. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:16}} community-submitted {{PLURAL:16|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, uploading large files to Wikimedia Commons has become more stable and less prone to failure following some fixes related to the “Could not acquire lock” upload error. [https://phabricator.wikimedia.org/T386640] '''Updates for technical contributors''' * Debian Bullseye will reach the end of its Long Term Support on 31 August 2026. [[phab:T434103|Some Cloud VPS projects]] still have instances running Debian Bullseye. Maintainers of those projects are encouraged to migrate to Debian Bookworm or Debian Trixie. A [[wikitech:Help:Cloud VPS instance operating system migration|migration guide]] is available to help with the process, and users may also want to consider whether their workload is better suited to Toolforge. If you need help or cannot complete the migration by 31 August, please contact the Cloud VPS admins as soon as possible. [[listarchive:list/cloud-announce@lists.wikimedia.org/thread/RVIPQSYLKMSL5M46JP6NEJVGVE6I2RXQ/|Read more]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.47/wmf.16|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/34|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W34"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 17 août 2026 à 23:03 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30922806 --> == Actualités techniques n° 2026-35 == <section begin="technews-2026-W35"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/35|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * La page [[Special:CreateAccount|Spécial:Créer_un_compte]] a été simplifiée dans le cadre des travaux en cours visant à moderniser le processus de création de compte. Le panneau affichant les statistiques du projet n'apparaît plus à côté du formulaire, que ce soit sur ordinateur ou sur mobile. Plusieurs tests portant sur la création de comptes ont montré qu'un formulaire plus simple aidait les nouveaux utilisateurs à mener à bien leur inscription. [https://phabricator.wikimedia.org/T433783] * Afin d'améliorer les performances de la page, les images ne se chargent désormais que lorsqu'elles sont visualisées. Cela signifie que les images situées plus bas dans un article ne se chargeront pas si le lecteur ne fait jamais défiler la page jusqu'à cette partie, ce qui peut avoir une incidence sur certains indicateurs liés aux images. [https://phabricator.wikimedia.org/T148047] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:42|la tâche soumise|les {{formatnum:42}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:42||s}} la semaine dernière]]. Par exemple, un problème empêchant l'affichage des vignettes d'images sur Abstract Wikipedia après le déplacement du fichier correspondant sur Wikimedia Commons a désormais été résolu. Les vignettes s'actualisent désormais correctement lorsque les fichiers sont déplacés. [https://phabricator.wikimedia.org/T433448] '''Actualités pour la contribution technique''' * La fiche d'informations sur l'utilisateur est une fonctionnalité qui permet aux modérateurs de consulter des informations sur les comptes d'utilisateurs. Jusqu'à présent, elle n'était disponible que dans des sections telles que l'historique des pages, les journaux et les modifications récentes. Désormais, il est également possible de [[mw:Special:MyLanguage/Help:Extension:CheckUser#User_Info_card_in_page_content|l'insérer dans le contenu d'une page]] à l'aide de la fonction d'analyse <bdi lang="zxx" dir="ltr"><code><nowiki>{{#uic:}}</nowiki></code></bdi>. Cela peut s'avérer particulièrement utile dans des modèles tels que [[:en:Template:Userlinks|<bdi lang="zxx" dir="ltr"><code><nowiki>{{Userlinks}}</nowiki></code></bdi>]] (ou leurs variantes spécialisées), car cela facilitera la compréhension du contexte relatif à un ou une utilisatrice sur les divers forums communautaires. La fiche ne s'affichera que pour les utilisateurs qui l'ont activée dans leurs [[Special:Preferences#mw-input-wpcheckuser-userinfocard-enable|préférences]]. [https://phabricator.wikimedia.org/T424466] * En raison des risques liés à la sécurité et à la confidentialité des utilisateurs, nous avons désactivé l'accès aux URL utilisant <bdi lang="zxx" dir="ltr"><code><nowiki>Special:MyPage</nowiki></code></bdi> (Spécial:Ma_page) avec l’argument <bdi lang="zxx" dir="ltr"><code><nowiki>action=raw</nowiki></code></bdi>. Si vous êtes concerné par cette mesure, demandez-vous s'il est possible d'adopter une autre approche. Les URL utilisant <bdi lang="zxx" dir="ltr"><code><nowiki>Special:MyPage</nowiki></code></bdi> restent accessibles et utilisables sans <bdi lang="zxx" dir="ltr"><code><nowiki>action=raw</nowiki></code></bdi>. Les URL de pages utilisateur spécifiées (par exemple, <bdi lang="zxx" dir="ltr"><code><nowiki>User:Myusername</nowiki></code></bdi>) peuvent toujours être utilisées avec <bdi lang="zxx" dir="ltr"><code><nowiki>action=raw</nowiki></code></bdi>. [https://phabricator.wikimedia.org/T120386] * Suite à une mise à jour, le logiciel de création de vignettes a été amélioré. En effet, <bdi lang="zxx" dir="ltr"><code><nowiki>librsvg</nowiki></code></bdi> a été mis à jour vers la version 2.60 et <bdi lang="zxx" dir="ltr"><code><nowiki>ImageMagick</nowiki></code></bdi> vers la version 7. Cela a permis la résolution d’un certain nombre de bugs de longue date liés à la création de vignettes, tels que des erreurs de rendu. [https://phabricator.wikimedia.org/T419815#12222841] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.17|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/35|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W35"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 24 août 2026 à 22:45 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30961208 --> == Actualités techniques n° 2026-36 == <section begin="technews-2026-W36"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/36|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * Un nouveau format pour la liste de souhaits de la communauté est ouvert aux commentaires. Vous pouvez [[m:Special:MyLanguage/Community Wishlist/Community Wishlist 2027|lire les idées proposées sur Meta]]. Ce nouveau processus prévoit d'améliorer la manière dont les souhaits sont triés, votés et priorisés d'une manière transparente et équilibrée entre les familles de projets et les éditions linguistiques. Cette consultation est ouverte pendant deux semaines. '''Actualités pour la contribution''' * La dernière version de l'application Wikipédia pour Android comprend des mises à jour de la fonctionnalité «Enregistrés», qui rapprochent l'expérience d'enregistrement de l'application de celle proposée sur iOS et sur le Web. Cette mise à jour repense l'onglet «Enregistrés» en proposant une vue «Tous les articles», supprime la liste de lecture par défaut «Enregistrés», renomme les listes de lecture «Collections» et modernise l'expérience de sauvegarde des articles. [https://phabricator.wikimedia.org/T420788] * La fonctionnalité [[mw:Special:MyLanguage/Readers/Reader Experience/Reading lists|Listes de lecture]] a été activée pour tous les utilisateurs connectés sur les versions de Wikipédia en bengali, chinois, tchèque et vietnamien le 25 août, après plusieurs mois en tant que fonctionnalité bêta. Les listes de lecture seront disponibles pour tous les utilisateurs connectés sur les versions de Wikipédia en arabe, français et indonésien le 1er septembre, suivies par la Wikipédia en anglais le 14 septembre, et toutes les autres versions de Wikipédia le 28 septembre. * À la fin du mois, certains lecteurs non connectés sur les versions en bengali, tchèque, persan, anglais et polonais de Wikipédia utilisant l'habillage Minerva sur mobile verront une [[mw:Special:MyLanguage/Readers/Reader_Growth/Minimal_Minerva|barre de navigation mise à jour]] dans le cadre d'un [[w:A/B test|test A/B]]. Le test comparera la barre de navigation actuelle avec une nouvelle version conçue pour faciliter la recherche d'informations plus rapidement. L'objectif est de déterminer si ces changements encouragent les lecteurs à revenir plus souvent. Cette expérience n'aura aucune incidence sur l'expérience des lecteurs et/ou des rédacteurs connectés. * Les contributeurs qui gèrent les redirections, les modèles et les catégories utilisés sur les pages de redirection disposent désormais de meilleurs moyens pour trouver et organiser les redirections. Auparavant, il n'était pas possible de rechercher les pages de redirection. Deux nouveaux mots-clés de recherche, <bdi lang="zxx" dir="ltr"><code><nowiki>onlyredirects:</nowiki></code></bdi> et <bdi lang="zxx" dir="ltr"><code><nowiki>withredirects:</nowiki></code></bdi>, permettent désormais de rechercher directement les redirections et peuvent être combinés avec des mots-clés existants tels que <bdi lang="zxx" dir="ltr"><code><nowiki>incategory:</nowiki></code></bdi>, <bdi lang="zxx" dir="ltr"><code><nowiki>intitle:</nowiki></code></bdi> et <bdi lang="zxx" dir="ltr"><code><nowiki>insource:</nowiki></code></bdi>. [https://phabricator.wikimedia.org/T204089] * Les outils de recherche ISBN pour générer des citations ne fonctionnaient pas récemment en raison de problèmes de service externe. Les développeurs travaillent sur des solutions. [https://phabricator.wikimedia.org/T435179] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:30|la tâche soumise|les {{formatnum:30}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:30||s}} la semaine dernière]]. Par exemple, un problème où la recherche de pages par catégorie à l'aide de <bdi lang="zxx" dir="ltr"><code><nowiki>deepcat</nowiki></code></bdi> pouvait ne renvoyer aucun résultat ou des résultats sans rapport a été corrigé. [https://phabricator.wikimedia.org/T414859] '''Actualités pour la contribution technique''' * Le domaine des URL pour les miniatures passe de upload.wikimedia.org à thumb.wikimedia.org. Les anciennes URL continueront de fonctionner pour le moment, mais MediaWiki utilisera le nouveau domaine à la place. Les URL vers d'autres types de médias tels que les fichiers originaux, les vidéos et les transcodages seront toujours servies depuis upload.wikimedia.org. [https://phabricator.wikimedia.org/T427465] * L'API Math de Wikimédia [https://www.mediawiki.org/w/index.php?api=wmf-math%2Fv1&title=Special%3ARestSandbox] est désormais obsolète. Ces points de terminaison seront complètement arrêtés d’ici la fin de septembre 2026. Les développeurs qui appellent ces points de terminaison devraient passer à des solutions alternatives de rendu mathématique, telles que le [https://developer.mozilla.org/en-US/docs/Web/MathML MathML] natif ou [https://www.mathjax.org/ MathJax]. Les installations tierces de MediaWiki utilisant l'extension Math pour le rendu des formules doivent effectuer une mise à jour vers la version 1.43+ afin d'éviter toute interruption de service. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.18|MediaWiki]] '''En détails''' * Lisez-en plus sur [[mw:Special:MyLanguage/Edit_check/TextMatch|TextMatch]] dans un article de Diff intitulé [[diffblog:2026/08/28/custom-edit-suggestions-for-every-wiki-how-communities-are-shaping-suggestion-mode-with-textmatch/|Suggestions de modifications personnalisées pour chaque wiki : comment les communautés façonnent le mode Suggestion avec TextMatch]]. '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/36|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W36"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 31 août 2026 à 22:53 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=30990659 --> == Actualités techniques n° 2026-37 == <section begin="technews-2026-W37"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/37|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * La fonctionnalité [[mw:Special:MyLanguage/Help:Growth/Tools/Add_a_link|Ajouter un lien]] a été améliorée pour les utilisateurs de Wikipédia en anglais, avec une meilleure détection des liens présentant des différences de majuscules. Cela permettra de réduire les suggestions ambiguës dues aux différences de majuscules dans les titres d'articles, notamment ceux consacrés aux biens culturels. Une deuxième phase d'améliorations est prévue, qui permettra également de préparer cette fonctionnalité en vue de son déploiement dans d'autres langues. [https://phabricator.wikimedia.org/T435526#12240876] [https://phabricator.wikimedia.org/T434259] * La fonctionnalité [[mw:Special:MyLanguage/Article_guidance|Guide des articles]] sera activée par défaut pour les contributeurs débutants sur Wikipédia en anglais simplifié et en turc à compter du 10 septembre 2026, à la suite d'[[mw:Special:MyLanguage/Article_guidance/Updates#Summary_of_experiment_result|une expérience]]. Les contributeurs débutants ayant effectué entre 0 et 99 modifications verront automatiquement s'afficher cette fonctionnalité lorsqu'ils cliqueront sur un lien rouge ou utiliseront l'option « [[tr:Vikipedi:Madde_sihirbazı|Madde oluşturto]] » sur Wikipédia en turc pour créer un nouvel article. Ce changement vise à aider les contributeurs débutants à créer des articles de meilleure qualité, conformes aux normes de chaque Wikipédia. [[phab:maniphest/query/z3tcTjmLMxk5/#R|D'autres améliorations]] seront apportées en fonction des résultats de l'expérience et des retours de la communauté. * L'outil [https://pageviews.wmcloud.org « Analyse des pages vues »], qui permet aux utilisateurs de comparer le nombre de pages vues sur plusieurs pages, fête cette année ses 10 ans et s'est enrichi de plusieurs nouvelles fonctionnalités. Parmi celles-ci figurent [[toolforge:wikinav|WikiNav]], qui donne un aperçu de la manière dont les lecteurs de Wikipédia explorent le contenu, les statistiques d'édition dans [https://pageviews.wmcloud.org/siteviews?range=latest-30&sites=en.wikipedia.org « Vues du site »], la possibilité de [https://pageviews.wmcloud.org/massviews?source=wikiproject&project=en.wikipedia.org rechercher des articles appartenant à un projet Wiki] et la prise en charge du mode sombre. [https://phabricator.wikimedia.org/T378549] * Une barre de navigation [[mw:Special:MyLanguage/Readers/Reader_Growth/Minimal_Minerva|Minerva, visuellement simplifiée]] est actuellement testée auprès des lecteurs non connectés utilisant la version mobile des Wikipédias en bengali, tchèque, anglais, farsi et polonais. Cette expérience vise à déterminer si la simplification de la navigation permet de fidéliser davantage les lecteurs. Le test se déroulera du 31 août au 28 septembre et ne nécessite aucune intervention de la part des utilisateurs. * [[m:Special:MyLanguage/WMDE Technical Wishes|WMDE Technical Wishes]] travaille à l'amélioration des [[en:Wikipedia:VisualEditor/Named references|noms de référence générés automatiquement dans VisualEditor]]. Les contributeurs ne remarqueront qu'un léger changement à partir de cette semaine. Lors de l'ajout de noms de référence automatiques, la numérotation commencera par <bdi lang="zxx" dir="ltr"><code><nowiki>:1</nowiki></code></bdi> au lieu de <bdi lang="zxx" dir="ltr"><code><nowiki>:0</nowiki></code></bdi>. Pour en savoir plus, consultez la [[m:WMDE Technical Wishes/References/VisualEditor automatic reference names|page du projet]]. [https://gerrit.wikimedia.org/r/c/VisualEditor/VisualEditor/+/1332704] * Tous les wikis seront [[m:Special:MyLanguage/Tech/Server switch|en lecture seule pendant quelques minutes]] le 23 septembre. Cette opération est prévue à 14h UTC. De plus amples informations seront publiées dans la rubrique « Tech News » et seront également mises en ligne sur chaque wiki au cours des prochaines semaines. [https://phabricator.wikimedia.org/T433363] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:25|la tâche soumise|les {{formatnum:25}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:25||s}} la semaine dernière]]. Par exemple, un problème lié au fait que [[mw:Special:MyLanguage/Parsoid|Parsoid]] pouvait mal gérer les balises nowiki imbriquées, entraînant la perte de contenu et l'affichage de texte indésirable, a désormais été corrigé. [https://phabricator.wikimedia.org/T435116] '''Actualités pour la contribution technique''' * Les administrateurs de l'interface peuvent configurer les gadgets à partir de [[MediaWiki:Gadgets-definition]]. Le format de définition a été mis à jour et ne nécessite plus l'option <bdi lang="zxx" dir="ltr"><code><nowiki>ResourceLoader</nowiki></code></bdi>, car les gadgets sont toujours chargés via <bdi lang="zxx" dir="ltr"><code><nowiki>ResourceLoader</nowiki></code></bdi>. Cela simplifie la configuration des gadgets en supprimant une option qui n'est plus nécessaire, ce qui facilite la tâche des administrateurs lors de la définition des gadgets. [https://phabricator.wikimedia.org/T298199] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.19|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/37|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W37"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 7 septembre 2026 à 20:44 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=31008437 --> == <span lang="en" dir="ltr">Tech News: 2026-38</span> == <div lang="en" dir="ltr"> <section begin="technews-2026-W38"/><div class="plainlinks"> Latest '''[[m:Special:MyLanguage/Tech/News|tech news]]''' from the Wikimedia technical community. Please tell other users about these changes. Not all changes will affect you. [[m:Special:MyLanguage/Tech/News/2026/38|Translations]] are available. '''Weekly highlight''' * The Future Audiences team has started a [[en:Special:MyLanguage/Wikipedia:Village pump (proposals)#New proposed experiment to let readers know about “Google preferred sources”|discussion on English Wikipedia]] about a proposed new experiment to show a temporary notice about the new [https://blog.google/products-and-platforms/products/search/original-high-quality-content-search/ Google Preferred Sources] feature to Wikipedia readers coming from Google. The experiment is to test whether this notice makes visitors come back to Wikipedia more often. Preferred Sources feature lets users choose websites they trust so Google can highlight content from those sources more prominently in Search and AI experiences. We are interested in testing this on other languages that Google supports and would welcome assistance with starting conversations on other wikis. If interested, please reach out to Future Audiences on the [[m:Special:MyLanguage/Future Audiences/Preferred sources|talk page]]. '''Updates for editors''' * The chat platform Discord is releasing a new self-service framework that websites can use to specify how their links should look when shared on Discord. The Future Audiences team is planning to start using this new framework to improve how Wikipedia links look when shared on Discord, giving better attribution and credit to contributors. The link appearance and behavior won't change in the first phase of this project as we want to first collect some baseline data to assess the impact of future changes. However, if community members who are active on Discord spot any issues, they can reach out on [[phab:tag/future-audiences/|Phabricator]] or [[m:Special:MyLanguage/Future Audiences|Metawiki]]. * The Reader Growth team will be running an experiment to test whether [[mw:Special:MyLanguage/Readers/Reader_Growth/Compact Lead|compacting article lead sections on mobiles]] with the addition of a "read more" button improves reader retention. The test will begin on September 17 on Arabic, Spanish, French, Indonesian, Italian, Japanese, Portuguese, Vietnamese, and Chinese Wikipedias and will run for four weeks. * All wikis will be [[m:Special:MyLanguage/Tech/Server switch|read-only for a few minutes]] on September 23. This is planned at 14:00 UTC. More information will be published in Tech News and will also be posted on individual wikis in the coming week. [https://phabricator.wikimedia.org/T433363] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] View all {{formatnum:28}} community-submitted {{PLURAL:28|task|tasks}} that were [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|resolved last week]]. For example, an issue where [https://wikistats.wmcloud.org/ Wikistats] for Wikipedias was returning an HTTP 500 error and could not be reached, has now been fixed. [https://phabricator.wikimedia.org/T435959] '''Updates for technical contributors''' * Developers who maintain a tool that queries the Wikimedia Commons links tables need to update their code to connect to the new x4 database cluster. The links tables have been moved from the s4 cluster to x4, and will no longer receive updates on s4. The page and redirect tables remain available on both clusters. A wiki replica for the x4 cluster will be set up afterwards. The change is being made because the s4 cluster has grown too large to operate efficiently. You can [[wikitech:Special:MyLanguage/News/2026 Commons links tables database split|read more]]. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Recurrent item]] Detailed code updates later this week: [[mw:MediaWiki 1.47/wmf.20|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Tech news]]''' prepared by [[m:Special:MyLanguage/Tech/News/Writers|Tech News writers]] and posted by [[m:Special:MyLanguage/User:MediaWiki message delivery|bot]]&nbsp;• [[m:Special:MyLanguage/Tech/News#contribute|Contribute]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/38|Translate]]&nbsp;• [[m:Tech|Get help]]&nbsp;• [[m:Talk:Tech/News|Give feedback]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|Subscribe or unsubscribe]].'' </div><section end="technews-2026-W38"/> </div> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 14 septembre 2026 à 17:31 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=31039826 --> == Bascule de serveur : votre wiki sera bientôt en lecture seule durant un court instant == <section begin="server-switch"/><div class="plainlinks"> [[:m:Special:MyLanguage/Tech/Server switch|Lire ce message dans une autre langue]] • [https://meta.wikimedia.org/w/index.php?title=Special:Translate&group=page-Tech%2FServer+switch&language=&action=page&filter= {{int:please-translate}}] La [[foundation:|Fondation Wikimédia]] va basculer le trafic entre ses centres de données. Cela permettra de s’assurer que Wikipédia et les autres wikis de Wikimédia peuvent rester en ligne même après une catastrophe. Le trafic sera basculé le '''{{#time:j xg|2026-09-23|fr}}'''. La bascule débutera à '''[https://zonestamp.toolforge.org/{{#time:U|2026-09-23T14:00|en}} {{#time:H:i e|2026-09-23T14:00}}]'''. Malheureusement, en raison de certaines limites de [[mw:Special:MyLanguage/Manual:What is MediaWiki?|MediaWiki]], toutes les modifications de pages devront être arrêtées durant le passage d’un centre de données à l’autre. Nous nous excusons pour ce dérangement, que nous nous efforçons de réduire pour le futur. Une bannière sera affichée sur tous les wikis 30 minutes avant le début de l’opération. Cette bannière restera visible jusqu’à la fin de l’opération. Vous pouvez contribuer à la [https://meta.wikimedia.org/w/index.php?title=Special%3ATranslate&group=Centralnotice-tgroup-read_only_banner&task=view&language=&filter=&action=translate traduction ou relecture] du texte de cette bannière. '''Pendant une courte période, vous pourrez lire les wikis mais pas les modifier.''' * Vous ne pourrez pas effectuer de modification pendant une durée pouvant aller jusqu’à une heure, le {{#time:l j xg Y|2026-09-23|fr}}. * Si vous essayez de faire une modification ou de sauvegarder pendant cette période, vous verrez un message d’erreur. Nous espérons qu’aucune modification ne sera perdue durant ce temps, mais nous ne pouvons le garantir. Si vous voyez un message d’erreur, merci de patienter jusqu’au retour à la normale. Vous pourrez alors enregistrer votre modification. Nous vous conseillons cependant de faire une copie de votre modification avant, au cas où. ''Autres conséquences :'' * Les tâches de fond seront ralenties et certaines pourraient être stoppées. Les liens rouges ne seront pas mis à jour aussi vite que d’habitude. Si vous créez un article qui est déjà lié depuis une autre page, le lien rouge pourrait rester rouge plus longtemps que d’habitude. Certains scripts ayant un long temps d’exécution devront être stoppés. * Le déploiement de code devrait se dérouler comme chaque semaine. Cependant, certains codes particuliers pourraient être gelés si l’opération le nécessitait. * [[mw:Special:MyLanguage/GitLab|GitLab]] sera indisponible durant environ 90 minutes. Ce projet pourra être reporté si nécessaire. Vous pouvez [[wikitech:Switch_Datacenter|consulter le calendrier sur wikitech.wikimedia.org]]. Tout changement sera annoncé dans le calendrier. '''Merci de partager ces informations avec votre communauté.'''</div><section end="server-switch"/> [[m:user:Trizek (WMF)|Trizek (WMF)]] 15 septembre 2026 à 14:58 (CEST) <!-- Message envoyé par User:Trizek (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Distribution_list/Non-Technical_Village_Pumps_distribution_list&oldid=30513874 --> == Your feedback wanted: Remove ''(Language link added:) and similar'' edits from Special:RecentChanges == ''(Apologies for posting in English. You can help by translating it)'' Hello, I’m Danny from [[:en:w:Wikimedia_Deutschland|Wikimedia Deutschland’s]] [[m:Wikidata_For_Wikimedia_Projects|Wikidata For Wikimedia Projects]] team.<br/> We have been investigating and examining how Wikidata’s use in the [[Special:RecentChanges]] and [[Special:Watchlist]] pages can be made to be more useful, or less ‘noisy’ (fewer edits that have little value to the reader). We are currently exploring to make a change in the volume and the type of Wikidata edits that appear in these pages, and '''we need your feedback''' to help us ensure this is a positive change for the Wikis and won’t disrupt your workflows.<br/> Language links are also known as sitelinks or interlanguage links and describe when a Wikidata item is connected to a Wiki, adding it to the language dropdown tool for other connected Wikis. [[File:Types of Lang Link changes.png|x350px|frame|A variety of "Language Link" edits as seen in the Recent Changes feed]] The request for feedback is whether you support the removal of these language link edits from the Recent Changes and Watchlist feed, or would you oppose such a change and if so, why? Please share you thoughts on the dedicated '''[[m:Talk:Wikidata_For_Wikimedia_Projects/Clearer_Wikidata_Edit_Summaries/Hide_Language_Link_edits|metawiki Talk page]]'''. Further information about this change: [[m:Wikidata_For_Wikimedia_Projects/Clearer_Wikidata_Edit_Summaries/Hide_Language_Link_edits|m:Wikidata_For_Wikimedia_Projects/Hide_Language_Link_edits]] Thank you for any participation or feedback you give, -- [[m:User:Danny_Benjafield_(WMDE)|User:Danny Benjafield (WMDE)]] 25 septembre 2026 à 13:46 (CEST) <!-- Message envoyé par User:Danny Benjafield (WMDE)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=User:Danny_Benjafield_(WMDE)/MassMessage_test_list&oldid=31080832 --> == Actualités techniques n° 2026-39 == <section begin="technews-2026-W39"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/39|D’autres traductions]] sont disponibles. '''En lumière cette semaine''' * [[m:Special:MyLanguage/Tech/Server switch|Tous les wikis seront en lecture seule]] pendant quelques minutes le mercredi 23 septembre 2026 à [https://zonestamp.toolforge.org/1790172000 14 h 00 UTC]. Cette interruption est nécessaire pour effectuer des tests de sauvegarde dans le cadre de la bascule des serveurs du centre de données, [[wikitech:Special:MyLanguage/Deployments/Yearly calendar|qui a lieu deux fois par an]]. Pendant cette bascule, tout le trafic des sites web de Wikimédia est redirigé d'un centre de données principal vers le centre de données de secours afin de tester la disponibilité et d'éviter toute interruption de service, même en cas d'urgence. [https://phabricator.wikimedia.org/T433363] '''Actualités pour la contribution''' * L'équipe Growth a testé un nouvel avis post-édition destiné à [[mw:Special:MyLanguage/Contributors/Account Creation Experiments#4. Encourage Temporary Accounts to Register|encourager les titulaires de comptes temporaires à créer un compte permanent]]. Ce nouvel avis a remplacé plusieurs boîtes de dialogue et une notification de bienvenue simultanée par un message unique mettant en avant les avantages de la création d'un compte. L'expérience a permis d'augmenter le taux de création de comptes permanents de 1,94 % à 3,66 %, soit une hausse relative de 87 %. Cette modification va désormais être déployée sur tous les wikis. [https://phabricator.wikimedia.org/T433551] * Pour les contributeurs de Wikipédia utilisant la [[Special:Preferences#mw-prefsection-betafeatures|fonctionnalité bêta « Mode suggestion »]], il existe une [[Special:Preferences#mw-input-wpvisualeditor-editcheck-experimental|nouvelle préférence utilisateur à activer]] permettant d’afficher des contrôles d’édition et des suggestions « expérimentaux ». Ces types sont répertoriés sur la page [[Special:EditChecks#experimental-checks|Special:EditChecks]] et sont destinés aux premiers tests par les développeurs ainsi qu’à la collecte de retours d’expérience de la part d’utilisateurs expérimentés. Les administrateurs peuvent modifier les [[mw:Special:MyLanguage/Edit check/Configuration|détails de configuration]] de chaque suggestion comme d'habitude. Ils peuvent également [[mw:Special:MyLanguage/Help:Suggestion mode#Experimental Suggestions|utiliser cette fonctionnalité]] pour tester les idées de leur communauté concernant des contrôles et des suggestions créés localement. Les types expérimentaux bénéficieront d'un style visuel plus distinct dès la fin de cette semaine. [[mw:Talk:VisualEditor/Suggestion Mode|Vos retours sont les bienvenus]]. * Le [[m:CEE Technical Village Pump|CEE Technical Village Pump]] a été lancé afin d'offrir un espace dédié aux discussions techniques et à la collaboration entre les communautés Wikimédia d'Europe centrale et orientale. * Un nouvel outil, [[mw:Special:MyLanguage/Language Onboarding and Development/Starter kit|Starter Kit]], est désormais disponible pour les communautés Wikipédia de langues nouvelles ou peu répandues. Il rassemble des tâches guidées, des outils et des ressources destinés à aider les communautés à se lancer, à suivre leurs progrès et à collaborer avec l'ensemble de la communauté Wikimédia. Le Starter Kit est hébergé sur Wikimedia Toolforge et est destiné aux Wikipédias des langues comptant moins de 50 000 articles. Pour en savoir plus sur le fonctionnement du Starter Kit et sur la manière dont les communautés l'utilisent, consultez [[diffblog:2026/09/18/introducing-starter-kit-for-new-and-small-language-wikipedias/|cet article du blog Diff]]. * L'équipe chargée de la croissance du nombre de lecteurs lance une nouvelle phase de test de l'[[mw:Special:MyLanguage/Readers/Reader Growth/Image Browsing|expérience sur le carrousel d'images]]. Ce nouveau test utilisera trois nouvelles versions du design, mises à jour en fonction des commentaires de la communauté. L'équipe évaluera les résultats et déterminera avec les communautés s'il convient ou non de mettre en place cette fonctionnalité. L'expérience débutera la semaine du 28 septembre et se déroulera sur les Wikipédias en arabe, bengali, chinois, tchèque, anglais, farsi, français, allemand, indonésien, japonais, polonais, portugais, espagnol, suédois et vietnamien. * Après avoir été déployée avec succès sur les Wikipédias en arabe, en bengali, en chinois, en tchèque, en anglais, en français, en indonésien et en vietnamien, la [[mw:Special:MyLanguage/Readers/Reader Experience/Reading lists|fonctionnalité « Listes de lecture »]] développée par l'équipe chargée de l'expérience utilisateur sera accessible à tous les utilisateurs connectés sur l'ensemble des wikis Wikipédia à compter du 28 septembre 2026. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:28|la tâche soumise|les {{formatnum:28}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:28||s}} la semaine dernière]]. Par exemple, un problème lié au script de statut des mentors, qui mettait à jour de manière répétée le statut des mentors alors qu'aucun changement n'était intervenu, générant ainsi des entrées inutiles dans la section « Modifications récentes », a désormais été corrigé. [https://phabricator.wikimedia.org/T436659] '''Actualités pour la contribution technique''' * L'API de requête de liste <bdi lang="zxx" dir="ltr"><code><nowiki>abusefilters</nowiki></code></bdi>, utilisée pour récupérer des informations spécifiques sur les filtres anti-abus actifs ou historiques configurés sur un wiki, a été mise à jour afin de prendre en charge le paramètre de configuration <bdi lang="zxx" dir="ltr"><code><nowiki>formatversion=2</nowiki></code></bdi>, ce qui entraîne des changements majeurs dans la réponse de l'API. Tous les utilisateurs qui gèrent des scripts utilisateur ou tout autre code doivent vérifier s'ils utilisent l'API de requête de liste <bdi lang="zxx" dir="ltr"><code><nowiki>abusefilters</nowiki></code></bdi> et s'adapter à ces changements majeurs. [https://phabricator.wikimedia.org/T435828] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.21|MediaWiki]] '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/39|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W39"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 27 septembre 2026 à 13:56 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=31085309 --> == Actualités techniques n° 2026-40 == <section begin="technews-2026-W40"/><div class="plainlinks"> Dernières '''[[m:Special:MyLanguage/Tech/News|actualités techniques]]''' de la communauté technique de Wikimedia. N’hésitez pas à informer les autres utilisateurs de ces changements. Certains changements ne vous concernent pas. [[m:Special:MyLanguage/Tech/News/2026/40|D’autres traductions]] sont disponibles. '''Actualités pour la contribution''' * Les utilisateurs des skins par défaut sur le web pour ordinateur et mobile (respectivement Vector 2022 et Minerva) seront désormais informés si les suggestions de recherche affichées pendant la saisie dans la Recherche proviennent de redirections de pages. Auparavant, les utilisateurs pouvaient être confus lorsque les principales suggestions de recherche ne correspondaient pas exactement à ce qu'ils avaient tapé. La nouvelle notification de redirection précise que ces suggestions ne sont pas aléatoires, mais liées à la recherche. Les pages suggérées peuvent être des redirections vers les articles les plus pertinents recherchés. [https://phabricator.wikimedia.org/T303013] * Les utilisateurs de Wikipédia déconnectés sur le web mobile verront bientôt un bouton de signet pour enregistrer des articles dans leurs [[mw:Special:MyLanguage/Readers/Reader Experience/Reading lists|listes de lecture]] à la place du bouton Watchstar actuel. Lorsqu'on clique dessus, le bouton de signet invite les utilisateurs à se connecter ou à créer un compte pour enregistrer un article pour plus tard. Une expérience précédente a montré que l'icône de signet incitait quatre fois plus les utilisateurs déconnectés à créer un compte par rapport au bouton Watchstar. Ce changement fait partie des efforts pour encourager plus de lecteurs à devenir titulaires d'un compte et à utiliser des fonctionnalités qui les aident à sauvegarder et à revenir au contenu. [https://phabricator.wikimedia.org/T438769] * Une expérience commencera le 29 septembre sur les Wikipédias en espagnol, arabe, anglais et français pour simplifier l'expérience que les utilisateurs rencontrent immédiatement après la création de leur compte. Le test permettra de voir si réduire les obstacles du sondage de bienvenue actuel aide les nouveaux utilisateurs à comprendre plus facilement ce qu'ils doivent faire ensuite. L'expérience se déroulera jusqu'à la fin du mois d'octobre. [https://phabricator.wikimedia.org/T430058] * Le [[m:Special:MyLanguage/Product and Technology Advisory Council|Conseil consultatif sur les produits et la technologie]] a lancé un appel à la communauté pour suggérer des sujets qu'il devrait examiner et discuter avec la Wikimedia Foundation. Les éditeurs et contributeurs techniques sont invités à ajouter des sujets à [[m:Talk:Product and Technology Advisory Council|la page de discussion du conseil]]. * A la fin de cette semaine, il sera possible pour les communautés de configurer des [[mw:Special:MyLanguage/Help:Edit check|Vérifications et suggestions]] afin qu'elles soient [[mw:Special:MyLanguage/Edit check/Configuration#extraNamespaces|affichées dans des espaces de noms supplémentaires]], comme un espace de noms <bdi lang="zxx" dir="ltr"><code><nowiki>Draft:</nowiki></code></bdi>. [[mw:Talk:VisualEditor/Suggestion Mode|Vos retours sont les bienvenus]]. * Un bug dans la [[m:Special:GlobalWatchlist|Liste globale de surveillance]] empêche la page de se charger pour les utilisateurs qui ont des modifications non vues sur Wikimedia Commons. Les développeurs travaillent sur une solution. En attendant, les utilisateurs concernés peuvent suivre les solutions de contournement décrites dans la section d'aide [[mw:Special:MyLanguage/Extension:GlobalWatchlist#GlobalWatchlist temporarily broken for users with Wikimedia Commons|pertinente]]. * Les nouveaux mots magiques <bdi lang="zxx" dir="ltr"><code><nowiki>{{CATEGORYSORT:TIMESTAMP}}</nowiki></code></bdi> et <bdi lang="zxx" dir="ltr"><code><nowiki>{{CATEGORYSORT:RTIMESTAMP}}</nowiki></code></bdi> sont maintenant disponibles. Ils permettent de changer le tri par défaut des catégories pour qu'il soit basé sur la date et l'heure de la catégorisation. [https://phabricator.wikimedia.org/T433768] * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Voir {{PLURAL:20|la tâche soumise|les {{formatnum:20}} tâches soumises}} par la communauté [[m:Special:MyLanguage/Tech/News/Recently resolved community tasks|résolue{{PLURAL:20||s}} la semaine dernière]]. Par exemple, le problème où [https://wikistats.wmcloud.org/ Wikistats] pour Wikipédia n'était pas accessible a maintenant été résolu. [https://phabricator.wikimedia.org/T435959] '''Actualités pour la contribution technique''' * Le basculement du centre des données, prévu le 23 septembre, a été reporté. [[diffblog:2025/03/12/hear-that-the-wikis-go-silent-twice-a-year/|L'exercice de l'équinoxe]] a révélé des problèmes de capacité, empêchant le transfert de tous les services vers l'autre centre des données. Le processus a été mis en pause pour se concentrer sur l'investigation de ce problème, afin de continuer à pouvoir servir nos utilisateurs de manière fiable. Un nouvel exercice sera planifié. En attendant, tout le trafic et les modifications continuent de fonctionner normalement. * Le 23 septembre, une panne de courant a brièvement affecté tous les wikis, créant des problèmes intermittents en mode lecture et en mode édition. Cela a aussi affecté Gerrit. Cela n'a aucun lien avec le report du basculement du centre de données. [https://www.wikimediastatus.net/incidents/9fm19t8qykk8] * [[mw:Special:MyLanguage/Help:Extension:Produnto|Produnto]] a été déployé sur mediawiki.org et [[phab:T421436|certaines wikis en langues indiquées]]. Nous voulons avoir vos retours sur ce produit expérimental. Produnto permet aux utilisateurs d'utiliser des modules Lua hébergés sur [[gitlab:repos/lua|GitLab de Wikimedia]]. Il simplifie le partage du code Lua entre les wikis. Au lieu de copier chaque module Lua individuellement d'un wiki à l'autre, les utilisateurs pourront facilement partager des packages entre tous les wikis. * [[File:Reload icon with two arrows.svg|12px|link=|class=skin-invert|Sujet récurrent]] Détail des mises-à-jour à venir cette semaine : [[mw:MediaWiki 1.47/wmf.22|MediaWiki]] '''Rencontres et évènements''' * Un [[m:Event:Cross-wiki Code Collaboration Workshop|atelier de collaboration sur le code entre wikis]] aura lieu en ligne le 2 octobre à 12h30 UTC, réunissant des contributeurs techniques d'Asie du Sud pour découvrir Produnto et commencer à le tester sur les Wikipédias en Hindi, Punjabi, Odia, Telugu et Malayalam. Produnto est un gestionnaire de paquets pour déployer des modules Lua hébergés sur GitLab vers les wikis Wikimedia, et a récemment été déployé sur certains wikis pilotes. '''''[[m:Special:MyLanguage/Tech/News|Actualités techniques]]''' préparées par les [[m:Special:MyLanguage/Tech/News/Writers|rédacteurs des actualités techniques]] et postées par [[m:Special:MyLanguage/User:MediaWiki message delivery|robot]]. [[m:Special:MyLanguage/Tech/News#contribute|Contribuer]]&nbsp;• [[m:Special:MyLanguage/Tech/News/2026/40|Traduire]]&nbsp;• [[m:Tech|Obtenir de l’aide]]&nbsp;• [[m:Talk:Tech/News|Donner son avis]]&nbsp;• [[m:Global message delivery/Targets/Tech ambassadors|S’abonner ou se désabonner]].'' </div><section end="technews-2026-W40"/> <bdi lang="en" dir="ltr">[[User:MediaWiki message delivery|MediaWiki message delivery]]</bdi> 28 septembre 2026 à 12:49 (CEST) <!-- Message envoyé par User:STei (WMF)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=Global_message_delivery/Targets/Tech_ambassadors&oldid=31092225 --> == Wikidata Sitelink changes in Recent Changes and Watchlist == ''(Apologies for cross-posting in English, translating it will help spread the message)'' Editors on group 1 wikis, including Wikiquote, Wikibooks, Wikiversity, Wikivoyage, and Wikisource, will no longer see notifications on [[Special:RecentChanges|Recent Changes]] and [[Special:Watchlist|Watchlist]] when a link to ''another'' language version of a page is added, removed, or changed in its Wikidata item. This change will begin on '''October 5 at 07:00 UTC''' as a two-week trial to reduce high-volume, low-value notifications and make higher-value changes easier to identify. Editors who rely on these notifications should be aware of the change and can share feedback or concerns on the [[m:Wikidata For Wikimedia Projects/Clearer Wikidata Edit Summaries/Hide Language Link edits|project]] or [[m:Talk:Wikidata_For_Wikimedia_Projects/Clearer_Wikidata_Edit_Summaries/Hide_Language_Link_edits|talk page]].<br/>Please reach out if you have any questions, thank you! -- [[User:Danny_Benjafield_(WMDE)|Danny Benjafield (WMDE)]] ([[User_talk:Danny_Benjafield_(WMDE)|talk]]) sent via [[Utilisateur:MediaWiki message delivery|MediaWiki message delivery]] ([[Discussion utilisateur:MediaWiki message delivery|discussion]]) 1 octobre 2026 à 14:42 (CEST) <!-- Message envoyé par User:Danny Benjafield (WMDE)@metawiki en utilisant la liste sur https://meta.wikimedia.org/w/index.php?title=User:Danny_Benjafield_(WMDE)/MassMessage_test_list&oldid=31106967 --> h3zhkora7i5q9y5mx48773zfbnelbfv Fonctionnement d'un ordinateur/Les schedulers larges et leurs optimisations 0 84538 773847 773783 2026-10-01T12:22:04Z Mewtow 31375 773847 wikitext text/x-wiki Nous venons de parler de l'exécution dans le désordre pendant deux chapitres. Les chapitres précédents vous ont laissé penser qu'un processeur avec exécution dans le désordre était assez simple : on remplace le file de µops par un ''scheduler'', on ajoute éventuellement un unité de renommage de registres, et voilà ! Et en effet, l'implémentation la plus simple de l'OOO utilise un ''scheduler'' unique, appelée un '''''scheduler'' centralisé'''. Mais dans la réalité, les processeurs modernes n'utilisent pas un ''scheduler'' unique. Les processeurs modernes ont besoin de ''scheduler'' très grands, avec un grand nombre d'entrées. Or, nous avons vu qu'un ''scheduler'' est un sorte de mémoire associative très complexe, avec un grand nombre de comparateurs. Et cela pose problème quand le ''scheduler'' doit mémoriser un grand nombre de µops. Je reste volontairement vague sur le nombre de µops exact, car il dépend de la finesse de gravure du processeur. Plus les transistors sont petits et efficaces, plus les ''schedulers'' peuvent grossir sans trop de problème. Mais malgré la loi de Moore, il arrive que des processeurs aient besoin de ''schedulers'' plus larges que ce que peut leur offrir la technologie de l'époque. Le cout en transistors d'un ''scheduler'' n'est pas proportionnel au nombre de µops qu'elle peut mémoriser. Par exemple, si je compare un ''scheduler'' de 32 entrées (32 µops max), elle n'utilise pas deux fois plus transistors qu'un ''scheduler'' de 16 entrées. Le cout en circuit n'est pas deux fois plus élevé, mais est approximativement 4 fois plus élevé. Le cout est presque quadratique, à savoir qu'il est proportionnel au carré du nombre d'entrées. Et il en est de même avec le performance. Pour un grand nombre de µops, un ''scheduler'' centralisé n'est pas très pratique. Il consomme beaucoup de transistors, chauffe beaucoup, n'est pas très rapide. La solution est de scinder un gros ''scheduler'' en plusieurs ''schedulers'' plus petits. Pour faire la différence avec un ''scheduler'' unique, nous allons parler de ''scheduler'' centralisé s'il n'y en a qu'un, de '''''scheduler'' décentralisé''' s'il y en a plusieurs. ==La spécialisation des ''schedulers'' décentralisés== La scission du ''scheduler'' doit idéalement se faire en tenant compte du reste du processeur. En effet, il faut relier les ''schedulers'' aux unités de calcul, ainsi qu'à l'unité mémoire, et cela ne peut pas se faire n'importe comment. Reste à voir comment faire. Précisons d'or et déjà que la scission dépend de si le processeur supporte l'exécution dans le désordre des accès mémoire. Si le processeur accepte de faire les accès mémoire dans le désordre, alors la scission sera différente. ===Les ''schedulers'' spécialisés pour les µops flottantes=== Prenons un processeur où les accès mémoire sont réalisés dans l'ordre. Le processeur a donc un simple ''scheduler'' pour les µops entières, les µops flottantes et les µops de branchements. La scission la plus basique sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes. Il est aussi possible de séparer le ''scheduler'' pour les µops entières en deux : un pour les opérations de calcul et un autre pour les µops de branchements. La séparation est moins évidente, mais reste possible. Pour le moment, concentrons-nous sur la séparation du ''scheduler'' flottant et du ''scheduler entier''. L'avantage est que l'implémentation est très simple. Les flottants sont gérés à part des entiers. Ils ont leurs propres registres, des FPU séparées des ALU entières, bref : le séparation est complète. Utiliser un ''scheduler'' séparée va dans ce sens. Un avantage de cette scission se situe au niveau de la consommation d'électricité. Si le programme exécuté n'exécute aucune instruction flottante, le ''scheduler'' pour µops flottant peut être ''clock gate'', mis en veille, voire éteint. Le processeur consomme donc moins d'électricité, et chauffe moins. [[File:Processeur avec plusieurs fenêtres d'instruction.png|centre|vignette|upright=2.5|Processeur avec plusieurs ''schedulers''.]] Remarque importante : les deux ''schedulers'' travaillent indépendamment les unes des autres. Ils décident d'émettre une µops à chaque cycle, dans leur coin, sans rendre de compte à l'autre. La conséquence est qu'il est possible d'émettre deux µops par cycle, si les circonstances l'autorisent. Le ''scheduler'' entier émet une µops entière en même temps que le ''scheduler'' flottant émet une µops flottante. Les deux µops démarrent au même cycle, on n'émet pas l'une après l'autre. Le résultat est un gain en performance, sur lequel on reviendra plus tard. Un avantage de cette séparation est qu'elle permet de renommer les registres flottants et entiers séparément. Non pas que ce soit impossible avec un ''scheduler'' unique, mais c'est beaucoup plus compliqué et rarement fait. En séparant ''scheduler'' entier et ''scheduler'' flottant, les deux peuvent être précédés de deux unités de renommage séparées. Et à vrai dire, on peut même aller plus loin, en n'autorisant l'exécution dans le désordre que pour les opérations flottantes, ou au contraire que pour les opérations entières. La toute première implémentation de l'exécution dans le désordre, sur l'IBM 360, en est un bon exemple. La FPU de ce processeur était précédée par un ''scheduler'', mais les ALu entières n'étaient précédés que par une file de µops basique, sans exécution dans le désordre. ===Les ''schedulers'' spécialisés pour les µops mémoire=== Voyons maintenant le cas des processeurs où les accès mémoire sont exécutés dans le désordre. Il est possible d'utiliser un ''scheduler'' centralisé, ce qui était fait sur les anciens processeurs Intel pour donner un exemple. Mais voyons ce que cela peut donner avec un ''scheduler'' décentralisé. Une scission naturelle serait d'utiliser un ''scheduler'' pour les µops mémoire, et un ''scheduler'' pour le reste des µops. Les processeurs Intel de microarchitecture ''Skylake'' faisaient cela. Un avantage est que les deux ''schedulers'' sont plus spécialisés, ce qui entraine des économies de circuit. Les opérations entières/flottantes ont besoin de deux registres pour les opérandes et d'un autre résultat, alors que les µops mémoire n'ont besoin que de deux registres maximum. En utilisant un ''scheduler'' unique, chaque entrée doit mémoriser l'opcode et trois registres, dont un est inutilisé pour les µops mémoire et les branchements. En utilisant des ''schedulers'' séparés, celle pour les µops entières/flottantes a trois registres par entrée, contre seulement deux registres par entrée pour celle des µops mémoire. Un autre avantage est que cela permet d'émettre une µops mémoire et une autre µops lors du même cycle. Si les circonstances le permettent, le processeur peut émettre une µops entière dans une ALU entière, et une µops mémoire dans l'unité mémoire. Le gain en performance est substantiel. Précisons que les processeurs avec exécution dans le désordre peuvent exécuter une instruction arithmétique en parallèle d'un accès mémoire. Mais les deux ne démarrent pas au même cycle : on émet d'abord l'accès mémoire, puis les instructions de calcul suivent au cycle suivant. Là, les deux démarrent au même cycle. Une scission plus poussée sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes, un autre pour les µops mémoire. [[File:Processeur avec des fenêtres d'instruction décentralisées.png|centre|vignette|upright=2|Processeur avec des ''schedulers'' décentralisées]] ===Les stations de réservation=== Un cas extrême est celui où chaque ALU/FPU a sa propre ''scheduler''. Par exemple, il y a un ''scheduler'' pour l'ALU entière, un autre pour le multiplieur entier, un autre pour le ''barrel shifter'', un autre pour l'unité mémoire, un autre pour l'unité de branchements, un autre pour l'additionneur flottant, un autre pour le multiplier flottant, etc. Un exemple historique est celui de le FPU de l'IBM 360 modèle 90, qui avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient leur propre ''scheduler''. Un exemple aussi extrême n'est pas si rare. Les processeurs AMD utilisent beaucoup cette technique, pour les ALU entières, à quelques nuances près. A l'opposé, Intel préfère utiliser des ''schedulers'' centralisés sur les anciens processeurs, deux ou trois ''schedulers'' sur les processeurs récents (2020 et plus). Les processeurs AMD utilisaient précisément un ''scheduler'' par ALU entière, alors que les FPUs partagent un ''scheduler'' flottante unique. Mais nous verrons cela en détail dans le chapitre sur les processeurs superscalaires x86. Toujours est-il qu'un cas aussi unique porte un nom. Les ''schedulers'' reliés à une seule ALU (entière ou flottante) sont appelés des '''stations de réservation''', ''Reservation Stations'' en anglais. Le terme a été introduit dans l'article de Tomasulo, qui décrivait justement la FPU de l'IBM 360, mentionnée plus haut. Mais dans ce cours, le terme décrit des ''schedulers'' qui mémorisent les opérandes des instructions, pas seulement les noms de registres associés. [[File:Contournement avec des stations de réservation.png|centre|vignette|upright=2.5|Stations de réservation]] Toujours est-il que l'exécution dans le désordre avec des stations de réservations a un intérêt historique fort. Elle a été utilisée dans deux techniques d'exécution dans le désordre très ancienne : l'algorithme de Tomasulo et le ''Scoreboarding'' du processeur CDC 6600. Ces deux implémentations ont introduit la technique, qui a été raffinée par la suite. Leur défaut est qu'elles ne géraient pas les exceptions précises, ce qui fait qu'on verra ces deux techniques dans le chapitre suivant, qui leur est dédié. ==Les ''schedulers'' décentralisés versus centralisés== Le choix entre un ''scheduler'' centralisés et plusieurs petits ''schedulers'' décentralisés est un compromis entre économie de circuits et flexibilité. Un gros ''scheduler'' unique est plus flexible, ce qui peut entrainer des gains de performance, mais coute beaucoup en circuits et peut réduire la fréquence du processeur. ===Un ''scheduler'' centralisé est plus flexible, mais plus couteux=== Pour expliquer en quoi un gros ''scheduler'' unique est plus flexible, nous allons prendre un exemple. Comparons un ''scheduler'' de 64 entrées, et deux ''schedulers'' de 32 entrées chacun. Les deux ''schedulers'' sont spécialisé :l'un est spécialisé dans les µops entières, l'autre dans les µops flottantes. En tout, les deux ''schedulers'' peuvent mémoriser au maximum 32 µops entières et 32 µops flottantes. A l'opposé, le gros ''scheduler'' unique peut mémoriser 64 µops, peu importe leur nature. Si le programme exécuté n'utilise pas de nombres flottants, le différence devient flagrante. La solution à deux ''schedulers'' est limitée à 32 µops, alors que le ''scheduler'' centralisé peut mémoriser le double. Les performances seront donc meilleures avec le ''scheduler'' centralisé. Et au-delà de ça, le ''scheduler'' décentralisé est utilisé de manière optimale si la moitié des µops sont des µops entières, et l'autre moitié des µops flottantes. Alors que le ''scheduler'' centralisé se moque de la répartition entre µops entières et flottantes. Pour le dire autrement, avec un ''scheduler'' décentralisé, il arrive qu'un ''scheduler'' soit remplis, alors que les autres sont sous-utilisés, voire vides. Ce qui n'arrive pas avec un ''scheduler'' centralisé. Par contre, les ''schedulers'' décentralisés utilisent plusieurs ''schedulers'' de "petite taille", plus rapides, plus économes en circuits et économes en énergie. Le compromis n'est donc pas entre un ''scheduler'' à 64 entrées versus deux ''schedulers'' de 32 entrées. Le compromis en pratique est entre un ''scheduler'' de 64 entrées versus 3 ''schedulers'' de 32 entrées. Un exemple pratique est celui de le microarchitecture ''Skyleke'' d'Intel. Intel est passé d'un ''scheduler'' centralisé pre-''Skyleke'' à deux ''schedulers'' sur ''Skyleke'', mais le nombre total d'entrées était globalement le même entre les deux, même si l'évolution de le finesse de gravure entre les deux biaise un peu les résultats. ===L'émission avec un ''scheduler'' décentralisé : ''dispatch'' et ''schedule''=== Un point important est qu'avec des ''schedulers'' décentralisés, l'émission est découpée en deux étages. Le premier étage envoie une µops vers le ''scheduler'' adéquat, le seconde émet la µops quand ses opérandes sont prêts. La première étape fait rentrer une µops dans le ''scheduler'', la seconde l'en fait sortir. Le premier étage est l'étage de '''''dispatch''''', le second est l'étage de '''''scheduling'''''. Le ''dispatch'' analyse la µops et décide où l'envoyer, selon que c'est une µops mémoire, flottante, entière. Le ''scheduling'' gère le disponibilité des opérandes et des unités de calcul. La µop émise est ajoutée au tampon de réordonnancement, s'il existe, lors de l'étape de ''dispatch''. Cette séparation fait qu'il est possible de démarrer l'exécution de plusieurs µops simultanément : une µop par ''scheduler''. Le fait est que deux ''schedulers'' différents émettent leurs µops sans se synchroniser avec les autres. Deux ''schedulers'' peuvent donc émettre chacun une µops, ce qui fait deux µops émises lors du même cycle. Cela permet d'exécuter en même temps deux ou trois instructions 1-cycle, comme des additions, soustractions, décalages, etc. Il devient aussi possible d'émettre un µop entière en même temps qu'un µop flottante, si les µops entières ont un ''scheduler'' séparée de celle des µops flottantes. Notons que cela permet d'émettre deux µops par cycle au niveau de l'étape de ''scheduling'', pas à l'étape de ''dispatch''. Aussi, nous parlerons de '''''multi-schedule'''''. Par contre, il faut faire une précision importante : le processeur ne peut toujours qu'émettre une ''instruction'' par cycle. Le processeur charge et décode une instruction par cycle, pas plus. Pour faire une comparaison, rappelez-vous l'exécution dans le désordre simple. Elle permet d'exécuter plusieurs instructions par cycle d'horloge, si on regarde ce qui se passe au niveau des unités de calcul. Mais si on fait la moyenne sur le temps long, on ne peut pas dépasser un IPC (''instruction per cycle'') de 1. Le ''multi-scheduling'' est similaire. Il permet de gagner en performance, mais ne permet pas de dépasser un IPC de 1. Il augmente l'IPC quand il est bas, mais il ne faut que le faire remonter vers le seuil de 1. ==Les files de µops et l'exécution dans le désordre=== Depuis au moins trois chapitres, je vous ai fait croire que le ''scheduler'' remplaçait la file de micro-opérations, la file de µops. Plus précisémment, que le ''scheduler'' était une file de µops améliorée. Mais dans les faits, il est parfaitement possible d'utiliser une file de µops en complément d'un ''scheduler'', voire de plusieurs. Il est même possible de faire de l'exécution dans le désordre en utilisant uniquement des files de µops. Il est temps de voir comment ce genre de ruse est possible ===L'exécution dans le désordre avec des files de µops=== Prenez un ''scheduler'' décentralisé. Une optimisation consiste à remplacer les ''schedulers'' par des files de µops. L'idée est qu'un fois décodée, les instructions sont accumulées dans des files de µops séparées. Il y a typiquement un file de micro-opération pour les opérations entières, un autre pour les opérations flottantes, un autre pour les accès mémoire. D'autres processeurs utilisent un file pour les accès mémoire et un autre pour les autres instructions, comme le faisait le Pentium 4 d'Intel. Les files de µops ont chacune leur propre ''scoreboard'' pour détecter les dépendances. Les ''scoreboard'' des différentes files de µops communiquent entre eux pour gérer le disponibilité des registres. Quand un registre est lu ou écrit par un instruction, toutes les files de µops sont prévenues et mettent à jour leurs ''scoreboards'' en même temps. Le résultat est un processeur qui implémente une forme limitée d'exécution dans le désordre. Par exemple, prenons un processeur avec trois files de µops : une en aval de l'unité mémoire, une autre en aval de la FPU, et une autre en aval de l'ALU entière. Le processeur exécute les µops mémoire dans l'ordre, les µops entières dans l'ordre, et idem pour les µops flottantes. Par contre, une µop entière peut passer avant une µop mémoire, et réciproquement. Idem avec les µops flottantes, qui peuvent passer avant ou après une µops entières, comparé à l'ordre du programme. L'exécution dans le désordre est donc partielle. L'intérêt par rapport à l'exécution dans l'ordre est que si une dépendance bloque un FIFO, les autres peuvent continuer d'émettre des instructions. Les files de micro-opération émettent leurs instructions de manière indépendante, elles ne savent pas si telle µop dans un autre file doit passer avant ou après la µop qu'elles émettent. Par exemple, le processeur Pentium 4 avait deux files de µops séparées : un pour les µops mémoire et une autre pour les autres µops. Si jamais une µops mémoire bloquait le pipeline, l'autre file de µops pouvait continuer à émettre des instructions indépendante de le lecture/écriture bloquante. Un problème est qu'il est facile de se retrouver avec toutes les files de µops bloquées. Quand l'une est bloquée, les autres tendent à se vider rapidement, particulièrement quand c'est le file pour les accès mémoire qui se bloque. Le résultat est que les gains performances sont intermédiaires entre l'exécution dans l'ordre et l'exécution dans le désordre complète. L'avantage est que les files de µops sont plus économes en termes de transistors. Les ''schedulers'' sont des circuits complexes et couteux en circuits, quand on les compare à une file de µops de même taille. Le remplacement fait donc faire des économies de circuit substantielles, qu'on peut utiliser pour agrandir la file de µops résultante. Il y a donc le choix entre trois petits ''schedulers'' et trois grandes files de µops. L'agrandissement des files de µops compense partiellement le gain lié au fait que l'exécution dans le désordre est incomplète. Pour résumer, il existe 4 grandes techniques d'exécution dans le désordre. Elles peuvent se classer sur deux critères : est-ce que les instructions sont émises dans l'ordre, et combien il y a de files de micro-opération. {|class="wikitable" |- ! ! File de µops normale ! ''scheduler'' |- ! un seule | ''Scoreboard'' | ''scheduler'' centralisée |- ! Plusieurs | Plusieurs FIFOs, plusieurs ''Scoreboard'' indépendants | ''scheduler'' décentralisé |} ===L'usage de files d'instruction en complément des ''schedulers''=== S'il est possible de remplacer des ''schedulers'' par des files de µops, il est possible d'utiliser les deux en même temps, de manière complémentaires. L'idée est de faire précéder le ''scheduler'' par une file de µops. Ainsi, si le ''scheduler'' est remplit, l'unité d'émission peut accumuler des µops dans la file de µops, sans bloquer le pipeline. La technique a été introduite par les processeurs Intel et AMD modernes, vers la moitié des années 2020. Ils ont introduits des '''''non-scheduling queue''''' sur la plupart de leurs microarchitectures. Une autre manière de voir la situation est qu'au lieu d'utiliser un gros ''scheduler'' capable d'accumuler un grand nombre de µops, on utilise un ''sheduler'' plus petit couplé à une file de µops. Utiliser un gros ''scheduler'' demanderait trop de transistors, pour un gain en performance qui ne se manifesterait que dans des situations extrêmes, où le pipeline est bloqué pendant très longtemps. Utiliser un ''scheduler'' de taille modéré, qu'on complémente avec une file de µops pour gérer ces cas extrêmes, est un meilleur compromis. Les ''non-scheduling queue'' peuvent s'utiliser avec des ''schedulers'' décentralisés. Prenons un exemple avec trois ''schedulers'', un pour les µops mémoire, un autre pour l'ALU entière, et un autre pour la FPU. Il est possible de faire précéder chaque ''scheduler'' par sa propre ''non-scheduling queue''.Mais dans les faits, ce n'est pas ce qui est fait de nos jours. Autant les ''schedulers'' de l('unité mémoire et de la FPU disposent de leur ''non-scheduling queue'', autant ce n'est pas le cas pour celui lié à l'ALU entière. La raison est que les opérations entières sont plus rapides que les opérations flottantes, et les accès mémoire. Une opération entière prend quelques cycles d'horloge, alors qu'une opération flottante atteint la dizaine de cycles et les accès mémoire la centaine. Les µops mémoire tendent donc à s'accumuler plus vite que les autres, et le ''scheduler'' de lunité mémoire se remplit très vite. Il en est de même pour le ''scheduler'' flottant, mais il se remplit moins vite et a moins de chance d'être débordé. En tout cas, le ''scheduler'' entier, s'il a une taille suffisante, ne sera jamais remplit avant les autres. Il a peu de chances d'être bloqué parce que plein, il sera bloqué car les ''non-scheduling queue'' déborderont et bloqueront le pipeline avant. ===Le découplage décodage-renommage=== Les processeurs modernes utilisent une file de µops en aval de l'unité de renommage. Elle n'a pas de lien direct avec l'exécution dans le désordre, elle ne remplace pas un ''scheduler'', ni ne le complète. A la place, elle sert en quelque sorte de lubrifiant entre le décodeur d'instruction et l'unité de renommage. Nous parlerons de '''découplage décodage-renommage''' dans ce qui suit. Voyons en quoi il consiste. Une instruction, une fois décodée, donne une ou plusieurs µops. L’unité de renommage, quant à elle, traite une µop à la fois (sauf sur les CPU superscalaires, qu'on verra dans le prochain chapitre). Il y a donc un ''mismatch'' entre sortie des décodeurs et entrée de l'unité de renommage. Pour régler ce problème, on insère la file de µops entre le décodeur et l'unité de renommage. Le décodeur peut accumuler plusieurs µops par cycle dans la file de µops, mais elles sortiront une par une. L'unité de renommage pourra donc aller à son rythme de une µop par cycle, pendant que l'unité de décodage produira autant de µops par cycle que nécessaire. La file de µops sert donc à découpler le décodage et l'unité de renommage, à savoir les faire fonctionner chacun à leur rythme. [[File:Découplage du renommage et du décodage.png|centre|vignette|upright=2|Découplage du renommage et du décodage.]] Un autre avantage de cette file de µops est qu'elle peut implémenter des optimisations qu'un ''scheduler'' ne peut pas faire. Par exemple, elle peut implémenter un ''Loop Stream Detector'', qui permet de ne pas charger/décoder les isntructions de petites boucles. Si une boucle tient complétement dans la file de µops, ses µops sont lues depuis la file de µops, et non depuis le cache. Un ''scheduler'' en est incapable. Le rôle de découplage décodage-renommage est distinct du fait d'accumuler des µops quand le ''scheduler'' est plein. En pratique, ils sont remplis par des files de µops séparées. Les processeurs modenres disposent de ''non-scheduling queue'' séparées de la file de µops proprement dite. Les ''non-scheduling queue'' sont places après l'unité de renommage de registre, alors que la file de µops est par nature avant l'unité de renommage. Les processeurs Intel, AMD, et potentiellement ARM, avaient une file de µops, mais pas de ''non-scheduling queue''. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le scoreboarding et l'algorithme de Tomasulo | prevText=Le scoreboarding et l'algorithme de Tomasulo | next=Le scoreboarding et l'algorithme de Tomasulo | nextText=Le scoreboarding et l'algorithme de Tomasulo }} </noinclude> 5nxzew3l96p5jzdhc2g28n5fh7v15iq 773873 773847 2026-10-01T16:45:27Z Mewtow 31375 /* Les files de µops et l'exécution dans le désordre= */ 773873 wikitext text/x-wiki Nous venons de parler de l'exécution dans le désordre pendant deux chapitres. Les chapitres précédents vous ont laissé penser qu'un processeur avec exécution dans le désordre était assez simple : on remplace le file de µops par un ''scheduler'', on ajoute éventuellement un unité de renommage de registres, et voilà ! Et en effet, l'implémentation la plus simple de l'OOO utilise un ''scheduler'' unique, appelée un '''''scheduler'' centralisé'''. Mais dans la réalité, les processeurs modernes n'utilisent pas un ''scheduler'' unique. Les processeurs modernes ont besoin de ''scheduler'' très grands, avec un grand nombre d'entrées. Or, nous avons vu qu'un ''scheduler'' est un sorte de mémoire associative très complexe, avec un grand nombre de comparateurs. Et cela pose problème quand le ''scheduler'' doit mémoriser un grand nombre de µops. Je reste volontairement vague sur le nombre de µops exact, car il dépend de la finesse de gravure du processeur. Plus les transistors sont petits et efficaces, plus les ''schedulers'' peuvent grossir sans trop de problème. Mais malgré la loi de Moore, il arrive que des processeurs aient besoin de ''schedulers'' plus larges que ce que peut leur offrir la technologie de l'époque. Le cout en transistors d'un ''scheduler'' n'est pas proportionnel au nombre de µops qu'elle peut mémoriser. Par exemple, si je compare un ''scheduler'' de 32 entrées (32 µops max), elle n'utilise pas deux fois plus transistors qu'un ''scheduler'' de 16 entrées. Le cout en circuit n'est pas deux fois plus élevé, mais est approximativement 4 fois plus élevé. Le cout est presque quadratique, à savoir qu'il est proportionnel au carré du nombre d'entrées. Et il en est de même avec le performance. Pour un grand nombre de µops, un ''scheduler'' centralisé n'est pas très pratique. Il consomme beaucoup de transistors, chauffe beaucoup, n'est pas très rapide. La solution est de scinder un gros ''scheduler'' en plusieurs ''schedulers'' plus petits. Pour faire la différence avec un ''scheduler'' unique, nous allons parler de ''scheduler'' centralisé s'il n'y en a qu'un, de '''''scheduler'' décentralisé''' s'il y en a plusieurs. ==La spécialisation des ''schedulers'' décentralisés== La scission du ''scheduler'' doit idéalement se faire en tenant compte du reste du processeur. En effet, il faut relier les ''schedulers'' aux unités de calcul, ainsi qu'à l'unité mémoire, et cela ne peut pas se faire n'importe comment. Reste à voir comment faire. Précisons d'or et déjà que la scission dépend de si le processeur supporte l'exécution dans le désordre des accès mémoire. Si le processeur accepte de faire les accès mémoire dans le désordre, alors la scission sera différente. ===Les ''schedulers'' spécialisés pour les µops flottantes=== Prenons un processeur où les accès mémoire sont réalisés dans l'ordre. Le processeur a donc un simple ''scheduler'' pour les µops entières, les µops flottantes et les µops de branchements. La scission la plus basique sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes. Il est aussi possible de séparer le ''scheduler'' pour les µops entières en deux : un pour les opérations de calcul et un autre pour les µops de branchements. La séparation est moins évidente, mais reste possible. Pour le moment, concentrons-nous sur la séparation du ''scheduler'' flottant et du ''scheduler entier''. L'avantage est que l'implémentation est très simple. Les flottants sont gérés à part des entiers. Ils ont leurs propres registres, des FPU séparées des ALU entières, bref : le séparation est complète. Utiliser un ''scheduler'' séparée va dans ce sens. Un avantage de cette scission se situe au niveau de la consommation d'électricité. Si le programme exécuté n'exécute aucune instruction flottante, le ''scheduler'' pour µops flottant peut être ''clock gate'', mis en veille, voire éteint. Le processeur consomme donc moins d'électricité, et chauffe moins. [[File:Processeur avec plusieurs fenêtres d'instruction.png|centre|vignette|upright=2.5|Processeur avec plusieurs ''schedulers''.]] Remarque importante : les deux ''schedulers'' travaillent indépendamment les unes des autres. Ils décident d'émettre une µops à chaque cycle, dans leur coin, sans rendre de compte à l'autre. La conséquence est qu'il est possible d'émettre deux µops par cycle, si les circonstances l'autorisent. Le ''scheduler'' entier émet une µops entière en même temps que le ''scheduler'' flottant émet une µops flottante. Les deux µops démarrent au même cycle, on n'émet pas l'une après l'autre. Le résultat est un gain en performance, sur lequel on reviendra plus tard. Un avantage de cette séparation est qu'elle permet de renommer les registres flottants et entiers séparément. Non pas que ce soit impossible avec un ''scheduler'' unique, mais c'est beaucoup plus compliqué et rarement fait. En séparant ''scheduler'' entier et ''scheduler'' flottant, les deux peuvent être précédés de deux unités de renommage séparées. Et à vrai dire, on peut même aller plus loin, en n'autorisant l'exécution dans le désordre que pour les opérations flottantes, ou au contraire que pour les opérations entières. La toute première implémentation de l'exécution dans le désordre, sur l'IBM 360, en est un bon exemple. La FPU de ce processeur était précédée par un ''scheduler'', mais les ALu entières n'étaient précédés que par une file de µops basique, sans exécution dans le désordre. ===Les ''schedulers'' spécialisés pour les µops mémoire=== Voyons maintenant le cas des processeurs où les accès mémoire sont exécutés dans le désordre. Il est possible d'utiliser un ''scheduler'' centralisé, ce qui était fait sur les anciens processeurs Intel pour donner un exemple. Mais voyons ce que cela peut donner avec un ''scheduler'' décentralisé. Une scission naturelle serait d'utiliser un ''scheduler'' pour les µops mémoire, et un ''scheduler'' pour le reste des µops. Les processeurs Intel de microarchitecture ''Skylake'' faisaient cela. Un avantage est que les deux ''schedulers'' sont plus spécialisés, ce qui entraine des économies de circuit. Les opérations entières/flottantes ont besoin de deux registres pour les opérandes et d'un autre résultat, alors que les µops mémoire n'ont besoin que de deux registres maximum. En utilisant un ''scheduler'' unique, chaque entrée doit mémoriser l'opcode et trois registres, dont un est inutilisé pour les µops mémoire et les branchements. En utilisant des ''schedulers'' séparés, celle pour les µops entières/flottantes a trois registres par entrée, contre seulement deux registres par entrée pour celle des µops mémoire. Un autre avantage est que cela permet d'émettre une µops mémoire et une autre µops lors du même cycle. Si les circonstances le permettent, le processeur peut émettre une µops entière dans une ALU entière, et une µops mémoire dans l'unité mémoire. Le gain en performance est substantiel. Précisons que les processeurs avec exécution dans le désordre peuvent exécuter une instruction arithmétique en parallèle d'un accès mémoire. Mais les deux ne démarrent pas au même cycle : on émet d'abord l'accès mémoire, puis les instructions de calcul suivent au cycle suivant. Là, les deux démarrent au même cycle. Une scission plus poussée sépare un ''scheduler'' pour les µops de calcul entières, d'un ''scheduler'' pour les µops flottantes, un autre pour les µops mémoire. [[File:Processeur avec des fenêtres d'instruction décentralisées.png|centre|vignette|upright=2|Processeur avec des ''schedulers'' décentralisées]] ===Les stations de réservation=== Un cas extrême est celui où chaque ALU/FPU a sa propre ''scheduler''. Par exemple, il y a un ''scheduler'' pour l'ALU entière, un autre pour le multiplieur entier, un autre pour le ''barrel shifter'', un autre pour l'unité mémoire, un autre pour l'unité de branchements, un autre pour l'additionneur flottant, un autre pour le multiplier flottant, etc. Un exemple historique est celui de le FPU de l'IBM 360 modèle 90, qui avait un additionneur flottant, un multiplieur flottant, et une unité mémoire pour les lectures/écritures flottantes. Les trois unités avaient leur propre ''scheduler''. Un exemple aussi extrême n'est pas si rare. Les processeurs AMD utilisent beaucoup cette technique, pour les ALU entières, à quelques nuances près. A l'opposé, Intel préfère utiliser des ''schedulers'' centralisés sur les anciens processeurs, deux ou trois ''schedulers'' sur les processeurs récents (2020 et plus). Les processeurs AMD utilisaient précisément un ''scheduler'' par ALU entière, alors que les FPUs partagent un ''scheduler'' flottante unique. Mais nous verrons cela en détail dans le chapitre sur les processeurs superscalaires x86. Toujours est-il qu'un cas aussi unique porte un nom. Les ''schedulers'' reliés à une seule ALU (entière ou flottante) sont appelés des '''stations de réservation''', ''Reservation Stations'' en anglais. Le terme a été introduit dans l'article de Tomasulo, qui décrivait justement la FPU de l'IBM 360, mentionnée plus haut. Mais dans ce cours, le terme décrit des ''schedulers'' qui mémorisent les opérandes des instructions, pas seulement les noms de registres associés. [[File:Contournement avec des stations de réservation.png|centre|vignette|upright=2.5|Stations de réservation]] Toujours est-il que l'exécution dans le désordre avec des stations de réservations a un intérêt historique fort. Elle a été utilisée dans deux techniques d'exécution dans le désordre très ancienne : l'algorithme de Tomasulo et le ''Scoreboarding'' du processeur CDC 6600. Ces deux implémentations ont introduit la technique, qui a été raffinée par la suite. Leur défaut est qu'elles ne géraient pas les exceptions précises, ce qui fait qu'on verra ces deux techniques dans le chapitre suivant, qui leur est dédié. ==Les ''schedulers'' décentralisés versus centralisés== Le choix entre un ''scheduler'' centralisés et plusieurs petits ''schedulers'' décentralisés est un compromis entre économie de circuits et flexibilité. Un gros ''scheduler'' unique est plus flexible, ce qui peut entrainer des gains de performance, mais coute beaucoup en circuits et peut réduire la fréquence du processeur. ===Un ''scheduler'' centralisé est plus flexible, mais plus couteux=== Pour expliquer en quoi un gros ''scheduler'' unique est plus flexible, nous allons prendre un exemple. Comparons un ''scheduler'' de 64 entrées, et deux ''schedulers'' de 32 entrées chacun. Les deux ''schedulers'' sont spécialisé :l'un est spécialisé dans les µops entières, l'autre dans les µops flottantes. En tout, les deux ''schedulers'' peuvent mémoriser au maximum 32 µops entières et 32 µops flottantes. A l'opposé, le gros ''scheduler'' unique peut mémoriser 64 µops, peu importe leur nature. Si le programme exécuté n'utilise pas de nombres flottants, le différence devient flagrante. La solution à deux ''schedulers'' est limitée à 32 µops, alors que le ''scheduler'' centralisé peut mémoriser le double. Les performances seront donc meilleures avec le ''scheduler'' centralisé. Et au-delà de ça, le ''scheduler'' décentralisé est utilisé de manière optimale si la moitié des µops sont des µops entières, et l'autre moitié des µops flottantes. Alors que le ''scheduler'' centralisé se moque de la répartition entre µops entières et flottantes. Pour le dire autrement, avec un ''scheduler'' décentralisé, il arrive qu'un ''scheduler'' soit remplis, alors que les autres sont sous-utilisés, voire vides. Ce qui n'arrive pas avec un ''scheduler'' centralisé. Par contre, les ''schedulers'' décentralisés utilisent plusieurs ''schedulers'' de "petite taille", plus rapides, plus économes en circuits et économes en énergie. Le compromis n'est donc pas entre un ''scheduler'' à 64 entrées versus deux ''schedulers'' de 32 entrées. Le compromis en pratique est entre un ''scheduler'' de 64 entrées versus 3 ''schedulers'' de 32 entrées. Un exemple pratique est celui de le microarchitecture ''Skyleke'' d'Intel. Intel est passé d'un ''scheduler'' centralisé pre-''Skyleke'' à deux ''schedulers'' sur ''Skyleke'', mais le nombre total d'entrées était globalement le même entre les deux, même si l'évolution de le finesse de gravure entre les deux biaise un peu les résultats. ===L'émission avec un ''scheduler'' décentralisé : ''dispatch'' et ''schedule''=== Un point important est qu'avec des ''schedulers'' décentralisés, l'émission est découpée en deux étages. Le premier étage envoie une µops vers le ''scheduler'' adéquat, le seconde émet la µops quand ses opérandes sont prêts. La première étape fait rentrer une µops dans le ''scheduler'', la seconde l'en fait sortir. Le premier étage est l'étage de '''''dispatch''''', le second est l'étage de '''''scheduling'''''. Le ''dispatch'' analyse la µops et décide où l'envoyer, selon que c'est une µops mémoire, flottante, entière. Le ''scheduling'' gère le disponibilité des opérandes et des unités de calcul. La µop émise est ajoutée au tampon de réordonnancement, s'il existe, lors de l'étape de ''dispatch''. Cette séparation fait qu'il est possible de démarrer l'exécution de plusieurs µops simultanément : une µop par ''scheduler''. Le fait est que deux ''schedulers'' différents émettent leurs µops sans se synchroniser avec les autres. Deux ''schedulers'' peuvent donc émettre chacun une µops, ce qui fait deux µops émises lors du même cycle. Cela permet d'exécuter en même temps deux ou trois instructions 1-cycle, comme des additions, soustractions, décalages, etc. Il devient aussi possible d'émettre un µop entière en même temps qu'un µop flottante, si les µops entières ont un ''scheduler'' séparée de celle des µops flottantes. Notons que cela permet d'émettre deux µops par cycle au niveau de l'étape de ''scheduling'', pas à l'étape de ''dispatch''. Aussi, nous parlerons de '''''multi-schedule'''''. Par contre, il faut faire une précision importante : le processeur ne peut toujours qu'émettre une ''instruction'' par cycle. Le processeur charge et décode une instruction par cycle, pas plus. Pour faire une comparaison, rappelez-vous l'exécution dans le désordre simple. Elle permet d'exécuter plusieurs instructions par cycle d'horloge, si on regarde ce qui se passe au niveau des unités de calcul. Mais si on fait la moyenne sur le temps long, on ne peut pas dépasser un IPC (''instruction per cycle'') de 1. Le ''multi-scheduling'' est similaire. Il permet de gagner en performance, mais ne permet pas de dépasser un IPC de 1. Il augmente l'IPC quand il est bas, mais il ne faut que le faire remonter vers le seuil de 1. ==Les files de µops et l'exécution dans le désordre== Depuis au moins trois chapitres, je vous ai fait croire que le ''scheduler'' remplaçait la file de micro-opérations, la file de µops. Plus précisémment, que le ''scheduler'' était une file de µops améliorée. Mais dans les faits, il est parfaitement possible d'utiliser une file de µops en complément d'un ''scheduler'', voire de plusieurs. Il est même possible de faire de l'exécution dans le désordre en utilisant uniquement des files de µops. Il est temps de voir comment ce genre de ruse est possible ===L'exécution dans le désordre avec des files de µops=== Prenez un ''scheduler'' décentralisé. Une optimisation consiste à remplacer les ''schedulers'' par des files de µops. L'idée est qu'un fois décodée, les instructions sont accumulées dans des files de µops séparées. Il y a typiquement un file de micro-opération pour les opérations entières, un autre pour les opérations flottantes, un autre pour les accès mémoire. D'autres processeurs utilisent un file pour les accès mémoire et un autre pour les autres instructions, comme le faisait le Pentium 4 d'Intel. Les files de µops ont chacune leur propre ''scoreboard'' pour détecter les dépendances. Les ''scoreboard'' des différentes files de µops communiquent entre eux pour gérer le disponibilité des registres. Quand un registre est lu ou écrit par un instruction, toutes les files de µops sont prévenues et mettent à jour leurs ''scoreboards'' en même temps. Le résultat est un processeur qui implémente une forme limitée d'exécution dans le désordre. Par exemple, prenons un processeur avec trois files de µops : une en aval de l'unité mémoire, une autre en aval de la FPU, et une autre en aval de l'ALU entière. Le processeur exécute les µops mémoire dans l'ordre, les µops entières dans l'ordre, et idem pour les µops flottantes. Par contre, une µop entière peut passer avant une µop mémoire, et réciproquement. Idem avec les µops flottantes, qui peuvent passer avant ou après une µops entières, comparé à l'ordre du programme. L'exécution dans le désordre est donc partielle. L'intérêt par rapport à l'exécution dans l'ordre est que si une dépendance bloque un FIFO, les autres peuvent continuer d'émettre des instructions. Les files de micro-opération émettent leurs instructions de manière indépendante, elles ne savent pas si telle µop dans un autre file doit passer avant ou après la µop qu'elles émettent. Par exemple, le processeur Pentium 4 avait deux files de µops séparées : un pour les µops mémoire et une autre pour les autres µops. Si jamais une µops mémoire bloquait le pipeline, l'autre file de µops pouvait continuer à émettre des instructions indépendante de le lecture/écriture bloquante. Un problème est qu'il est facile de se retrouver avec toutes les files de µops bloquées. Quand l'une est bloquée, les autres tendent à se vider rapidement, particulièrement quand c'est le file pour les accès mémoire qui se bloque. Le résultat est que les gains performances sont intermédiaires entre l'exécution dans l'ordre et l'exécution dans le désordre complète. L'avantage est que les files de µops sont plus économes en termes de transistors. Les ''schedulers'' sont des circuits complexes et couteux en circuits, quand on les compare à une file de µops de même taille. Le remplacement fait donc faire des économies de circuit substantielles, qu'on peut utiliser pour agrandir la file de µops résultante. Il y a donc le choix entre trois petits ''schedulers'' et trois grandes files de µops. L'agrandissement des files de µops compense partiellement le gain lié au fait que l'exécution dans le désordre est incomplète. Pour résumer, il existe 4 grandes techniques d'exécution dans le désordre. Elles peuvent se classer sur deux critères : est-ce que les instructions sont émises dans l'ordre, et combien il y a de files de micro-opération. {|class="wikitable" |- ! ! File de µops normale ! ''scheduler'' |- ! un seule | ''Scoreboard'' | ''scheduler'' centralisée |- ! Plusieurs | Plusieurs FIFOs, plusieurs ''Scoreboard'' indépendants | ''scheduler'' décentralisé |} ===L'usage de files d'instruction en complément des ''schedulers''=== S'il est possible de remplacer des ''schedulers'' par des files de µops, il est possible d'utiliser les deux en même temps, de manière complémentaires. L'idée est de faire précéder le ''scheduler'' par une file de µops. Ainsi, si le ''scheduler'' est remplit, l'unité d'émission peut accumuler des µops dans la file de µops, sans bloquer le pipeline. La technique a été introduite par les processeurs Intel et AMD modernes, vers la moitié des années 2020. Ils ont introduits des '''''non-scheduling queue''''' sur la plupart de leurs microarchitectures. Une autre manière de voir la situation est qu'au lieu d'utiliser un gros ''scheduler'' capable d'accumuler un grand nombre de µops, on utilise un ''sheduler'' plus petit couplé à une file de µops. Utiliser un gros ''scheduler'' demanderait trop de transistors, pour un gain en performance qui ne se manifesterait que dans des situations extrêmes, où le pipeline est bloqué pendant très longtemps. Utiliser un ''scheduler'' de taille modéré, qu'on complémente avec une file de µops pour gérer ces cas extrêmes, est un meilleur compromis. Les ''non-scheduling queue'' peuvent s'utiliser avec des ''schedulers'' décentralisés. Prenons un exemple avec trois ''schedulers'', un pour les µops mémoire, un autre pour l'ALU entière, et un autre pour la FPU. Il est possible de faire précéder chaque ''scheduler'' par sa propre ''non-scheduling queue''.Mais dans les faits, ce n'est pas ce qui est fait de nos jours. Autant les ''schedulers'' de l('unité mémoire et de la FPU disposent de leur ''non-scheduling queue'', autant ce n'est pas le cas pour celui lié à l'ALU entière. La raison est que les opérations entières sont plus rapides que les opérations flottantes, et les accès mémoire. Une opération entière prend quelques cycles d'horloge, alors qu'une opération flottante atteint la dizaine de cycles et les accès mémoire la centaine. Les µops mémoire tendent donc à s'accumuler plus vite que les autres, et le ''scheduler'' de lunité mémoire se remplit très vite. Il en est de même pour le ''scheduler'' flottant, mais il se remplit moins vite et a moins de chance d'être débordé. En tout cas, le ''scheduler'' entier, s'il a une taille suffisante, ne sera jamais remplit avant les autres. Il a peu de chances d'être bloqué parce que plein, il sera bloqué car les ''non-scheduling queue'' déborderont et bloqueront le pipeline avant. ===Le découplage décodage-renommage=== Les processeurs modernes utilisent une file de µops en aval de l'unité de renommage. Elle n'a pas de lien direct avec l'exécution dans le désordre, elle ne remplace pas un ''scheduler'', ni ne le complète. A la place, elle sert en quelque sorte de lubrifiant entre le décodeur d'instruction et l'unité de renommage. Nous parlerons de '''découplage décodage-renommage''' dans ce qui suit. Voyons en quoi il consiste. Une instruction, une fois décodée, donne une ou plusieurs µops. L’unité de renommage, quant à elle, traite une µop à la fois (sauf sur les CPU superscalaires, qu'on verra dans le prochain chapitre). Il y a donc un ''mismatch'' entre sortie des décodeurs et entrée de l'unité de renommage. Pour régler ce problème, on insère la file de µops entre le décodeur et l'unité de renommage. Le décodeur peut accumuler plusieurs µops par cycle dans la file de µops, mais elles sortiront une par une. L'unité de renommage pourra donc aller à son rythme de une µop par cycle, pendant que l'unité de décodage produira autant de µops par cycle que nécessaire. La file de µops sert donc à découpler le décodage et l'unité de renommage, à savoir les faire fonctionner chacun à leur rythme. [[File:Découplage du renommage et du décodage.png|centre|vignette|upright=2|Découplage du renommage et du décodage.]] Un autre avantage de cette file de µops est qu'elle peut implémenter des optimisations qu'un ''scheduler'' ne peut pas faire. Par exemple, elle peut implémenter un ''Loop Stream Detector'', qui permet de ne pas charger/décoder les isntructions de petites boucles. Si une boucle tient complétement dans la file de µops, ses µops sont lues depuis la file de µops, et non depuis le cache. Un ''scheduler'' en est incapable. Le rôle de découplage décodage-renommage est distinct du fait d'accumuler des µops quand le ''scheduler'' est plein. En pratique, ils sont remplis par des files de µops séparées. Les processeurs modenres disposent de ''non-scheduling queue'' séparées de la file de µops proprement dite. Les ''non-scheduling queue'' sont places après l'unité de renommage de registre, alors que la file de µops est par nature avant l'unité de renommage. Les processeurs Intel, AMD, et potentiellement ARM, avaient une file de µops, mais pas de ''non-scheduling queue''. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le scoreboarding et l'algorithme de Tomasulo | prevText=Le scoreboarding et l'algorithme de Tomasulo | next=Le scoreboarding et l'algorithme de Tomasulo | nextText=Le scoreboarding et l'algorithme de Tomasulo }} </noinclude> jskc7s7q4ff7aa89fncwj4udezip8ha Mathc complexes/0a4 0 84546 773899 2026-10-01T20:31:42Z Xhungab 23827 news 773899 wikitext text/x-wiki [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/a277#Le fichier principal :| Application]] Calculons : e^(0) = cos(0) + i sin(0) = (1) + i (0) = 1 e^(-i PI/2) = cos(-PI/2) + i sin(-PI/2) = cos( PI/2) - i sin( PI/2) = (0) - i (1) = -i e^(-i PI) = cos(-PI) + i sin(-PI) = cos( PI) - i sin( PI) = (-1) - i (0) = -1 e^(-i 2 PI) = cos(-2 PI) + i sin(-2 PI) = cos( 2 PI) - i sin( 2 PI) = (1) - i (0) = 1 * Mesure principale de -3 PI = -3 PI + 2(2 PI) = PI e^(-i 3 PI) = cos(PI) + i sin(PI) = (-1) + i (0) = -1 * Mesure principale de -3/2 PI = -3/2 PI + (2 PI) = -3/2 PI + (4 PI/2) = PI/2 e^(-i 3/2 PI) = cos(PI/2) + i sin(PI/2) = (0) + i (1) = i * Mesure principale de -9/2 PI = -9/2 PI + 2(2 PI) = -9/2 PI + (8 PI/2) = -PI/2 e^(-i 9/2 PI) = cos(-PI/2) + i sin(-PI/2) = cos( PI/2) - i sin( PI/2) = (0) - i = -i * Mesure principale de a : -Pi < a <= PI ]-PI .. 0] et de [0 .. PI] Cela permet de parcourir tout le cercle. {{AutoCat}} jkrmo1903gb8gbjp6b0x9wzmc4gt5t5 773913 773899 2026-10-02T09:16:57Z Xhungab 23827 773913 wikitext text/x-wiki [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/a277#Le fichier principal :| Application]] Calculons : e^(0) = cos(0) + i sin(0) = (1) + i (0) = 1 e^(-i PI/2) = cos(-PI/2) + i sin(-PI/2) = cos( PI/2) - i sin( PI/2) = (0) - i (1) = -i e^(-i PI) = cos(-PI) + i sin(-PI) = cos( PI) - i sin( PI) = (-1) - i (0) = -1 e^(-i 2 PI) = cos(-2 PI) + i sin(-2 PI) = cos( 2 PI) - i sin( 2 PI) = (1) - i (0) = 1 * Mesure principale de -3 PI : -3 PI + 2(2 PI) = PI e^(-i 3 PI) = cos(PI) + i sin(PI) = (-1) + i (0) = -1 * Mesure principale de -3/2 PI : -3/2 PI + (2 PI) = -3/2 PI + (4 PI/2) = PI/2 e^(-i 3/2 PI) = cos(PI/2) + i sin(PI/2) = (0) + i (1) = i * Mesure principale de -9/2 PI : -9/2 PI + 2(2 PI) = -9/2 PI + (8 PI/2) = -PI/2 e^(-i 9/2 PI) = cos(-PI/2) + i sin(-PI/2) = cos( PI/2) - i sin( PI/2) = (0) - i = -i * Mesure principale de a : -Pi < a <= PI ]-PI .. 0] et de [0 .. PI] Cela permet de parcourir tout le cercle. {{AutoCat}} jaoe1szxgvspqlgrquwt2fpmvnjm3g1 Mathc complexes/0a5 0 84547 773915 2026-10-02T11:05:19Z Xhungab 23827 news 773915 wikitext text/x-wiki [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/a277#Le fichier principal :| Application]] '''La mesure principale de a :''' -Pi < a <= PI ]-PI .. 0] et de [0 .. PI] Cela permet de parcourir tout le cercle. '''Déterminer la mesure principale de l'angles en radians : -67PI/5''' Utilisons l'encadrement : -PI < '''-67PI/5''' + 2kPI <= +PI Calculons k : -PI'''+67PI/5''' < 2k PI <= +PI'''+67PI/5''' Simplifions : (-5+67)/5 < 2k <= (+5+67)/5 6,2 7 7,2 (62)/5 < 2k <= (72)/5 --|-------|-------|-- 62/10 < k <= 72/10 Choisissons pour k la valeur 7 - PI < -67PI/5 + 2(k)PI <= +PI - PI < -67PI/5 + 2'''(7)'''PI <= +PI - PI < '''('''-67PI + 2(7)(5)PI''')/5''' <= +PI - PI < (3PI)/5 <= +PI '''L'angle principale de -67PI/5 est 3PI/5''' {{AutoCat}} 2aws1ssn0ljliy0cs6gmpins86o7hrs 773916 773915 2026-10-02T11:06:16Z Xhungab 23827 773916 wikitext text/x-wiki [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/a277#Le fichier principal :| Application]] '''La mesure principale de a :''' -Pi < a <= PI ]-PI .. 0] et de [0 .. PI] Cela permet de parcourir tout le cercle. '''Déterminer la mesure principale de l'angles en radians : -67PI/5''' Utilisons l'encadrement : -PI < '''-67PI/5''' + 2kPI <= +PI Calculons k : -PI'''+67PI/5''' < 2kPI <= +PI'''+67PI/5''' Simplifions : (-5+67)/5 < 2k <= (+5+67)/5 6,2 7 7,2 (62)/5 < 2k <= (72)/5 --|-------|-------|-- 62/10 < k <= 72/10 Choisissons pour k la valeur 7 - PI < -67PI/5 + 2(k)PI <= +PI - PI < -67PI/5 + 2'''(7)'''PI <= +PI - PI < '''('''-67PI + 2(7)(5)PI''')/5''' <= +PI - PI < (3PI)/5 <= +PI '''L'angle principale de -67PI/5 est 3PI/5''' {{AutoCat}} nnojjzbqq7nb00tjy8a31p2cg9s0fvg 773917 773916 2026-10-02T11:07:33Z Xhungab 23827 773917 wikitext text/x-wiki [[Catégorie:Mathc complexes (livre)]] [[Mathc complexes/a277#Le fichier principal :| Application]] '''La mesure principale de a :''' -Pi < a <= PI ]-PI .. 0] et de [0 .. PI] Cela permet de parcourir tout le cercle. '''Déterminer la mesure principale de l'angles en radians : -67PI/5''' Utilisons l'encadrement : -PI < '''-67PI/5''' + 2kPI <= +PI Calculons k : -PI'''+67PI/5''' < 2kPI <= +PI'''+67PI/5''' Simplifions : (-5+67)/5 < 2k <= (+5+67)/5 6,2 7 7,2 (62)/5 < 2k <= (72)/5 --|-------|-------|-- 62/10 < k <= 72/10 Choisissons pour k la valeur 7 - PI < -67PI/5 + 2(k)PI <= +PI - PI < -67PI/5 + 2'''(7)'''PI <= +PI - PI < '''('''-67PI + 2(7)'''(5)'''PI''')/5''' <= +PI - PI < (3PI)/5 <= +PI '''L'angle principale de -67PI/5 est 3PI/5''' {{AutoCat}} 1qplh6zzx06levkvwkllhac4vjps4dw