À quoi ressemble le langage d’assemblage multicœur?

Il était une fois, pour écrire l’assembleur x86, par exemple, que vous aviez des instructions indiquant “charger le registre EDX avec la valeur 5”, “incrémenter le registre EDX”, etc.

Avec les processeurs modernes dotés de 4 cœurs (voire plus), au niveau du code machine, semble-t-il qu’il existe 4 processeurs distincts (ex., Y a-t-il seulement 4 registres “EDX” distincts)? Si oui, quand vous dites “incrémenter le registre EDX”, qu’est-ce qui détermine quel registre EDX du processeur est incrémenté? Existe-t-il un concept de “contexte CPU” ou de “thread” dans l’assembleur x86?

Comment fonctionne la communication / synchronisation entre les cœurs?

Si vous écriviez un système d’exploitation, quel mécanisme est exposé via le matériel pour vous permettre de planifier l’exécution sur différents cœurs? Est-ce une instruction spéciale privilégiée?

Si vous écriviez une VM optimisée de compilateur / bytecode pour un processeur multicœur, que devriez-vous savoir spécifiquement sur, par exemple, x86 pour le faire générer du code qui s’exécute efficacement sur tous les cœurs?

Quelles modifications ont été apscopes au code machine x86 pour prendre en charge les fonctionnalités multicœurs?

Ce n’est pas une réponse directe à la question, mais une réponse à une question qui apparaît dans les commentaires. Essentiellement, la question est de savoir quelle prise en charge du matériel pour le fonctionnement multithread.

Nicholas Flynt avait raison , du moins en ce qui concerne x86. Dans un environnement multi-thread (Hyper-threading, multi-core ou multi-processeurs), le thread Bootstrap (généralement thread 0 dans le core 0 dans le processeur 0) démarre l’extraction du code de l’adresse 0xfffffff0 . Tous les autres threads démarrent dans un état de veille spécial appelé Wait-for-SIPI . Dans le cadre de son initialisation, le thread principal envoie une interruption d’interprocesseur (IPI) spéciale sur l’APIC appelé SIPI (Startup IPI) à chaque thread se trouvant dans WFS. Le SIPI contient l’adresse à partir de laquelle ce thread doit commencer à récupérer du code.

Ce mécanisme permet à chaque thread d’exécuter du code à partir d’une adresse différente. Tout ce dont vous avez besoin est un support logiciel pour chaque thread pour configurer ses propres tables et files d’attente de messagerie. Le système d’exploitation les utilise pour effectuer la planification multithread réelle.

En ce qui concerne l’assemblage proprement dit, comme l’a écrit Nicholas, il n’y a pas de différence entre les assemblages pour une application à un seul thread ou à plusieurs threads. Chaque thread logique a son propre ensemble de registres, écrivant ainsi:

 mov edx, 0 

ne mettra à jour que EDX pour le thread en cours d’exécution . Il n’y a aucun moyen de modifier EDX sur un autre processeur en utilisant une seule instruction d’assemblage. Vous avez besoin d’un appel système pour demander au système d’exploitation de dire à un autre thread d’exécuter du code qui mettra à jour son propre EDX .

Si je comprends bien, chaque “core” est un processeur complet, avec son propre ensemble de registres. Fondamentalement, le BIOS démarre avec un cœur en cours d’exécution, puis le système d’exploitation peut “démarrer” d’autres cœurs en les initialisant et en les dirigeant vers le code à exécuter, etc.

La synchronisation est effectuée par le système d’exploitation. En règle générale, chaque processeur exécute un processus différent pour le système d’exploitation, de sorte que la fonctionnalité multithread du système d’exploitation est chargée de décider quel processus doit toucher quelle mémoire et que faire en cas de collision de mémoire.

Minimal exécutable Intel x86 exemple nu-métal

Exemple de métal nu runable avec tous les passe-partout requirejs . Toutes les pièces principales sont couvertes ci-dessous.

Testé sur Ubuntu 15.10 QEMU 2.3.0 et Lenovo ThinkPad T400.

Le Manuel Intel Volume 3 Guide de programmation du système – 325384-056FR Septembre 2015 couvre les SMP dans les chapitres 8, 9 et 10.

Tableau 8-1. “Diffuser la séquence INIT-SIPI-SIPI et le choix des délais d’attente” contient un exemple qui fonctionne essentiellement:

 MOV ESI, ICR_LOW ; Load address of ICR low dword into ESI. MOV EAX, 000C4500H ; Load ICR encoding for broadcast INIT IPI ; to all APs into EAX. MOV [ESI], EAX ; Broadcast INIT IPI to all APs ; 10-millisecond delay loop. MOV EAX, 000C46XXH ; Load ICR encoding for broadcast SIPI IP ; to all APs into EAX, where xx is the vector computed in step 10. MOV [ESI], EAX ; Broadcast SIPI IPI to all APs ; 200-microsecond delay loop MOV [ESI], EAX ; Broadcast second SIPI IPI to all APs ; Waits for the timer interrupt until the timer expires 

Sur ce code:

  1. La plupart des systèmes d’exploitation rendront la plupart de ces opérations impossibles à partir de l’anneau 3 (programmes utilisateur).

    Vous devez donc écrire votre propre kernel pour jouer librement avec lui: un programme Linux utilisateur ne fonctionnera pas.

  2. Au début, un seul processeur s’exécute, appelé le processeur d’amorçage (BSP).

    Il doit réveiller les autres (appelés processeurs d’application (AP)) via des interruptions spéciales appelées interruptions entre processeurs (IPI) .

    Ces interruptions peuvent être effectuées en programmant le contrôleur APIC (Advanced Programmable Interrupt Controller) via le registre de commande d’interruption (ICR).

    Le format de l’ICR est documenté à: 10.6 “EMISSION D’INTERRUPTIONS D’INTERPROCESSEUR”

    L’IPI se produit dès que nous écrivons à l’ICR.

  3. ICR_LOW est défini au 8.4.4 “Exemple d’initialisation MP” comme suit:

     ICR_LOW EQU 0FEE00300H 

    La valeur magique 0FEE00300 correspond à l’adresse mémoire de l’ICR, comme 0FEE00300 dans le Tableau 10-1 “Carte d’adresse du registre APIC local”

  4. La méthode la plus simple est utilisée dans l’exemple: elle configure l’ICR pour envoyer des IPI de diffusion qui sont dissortingbués à tous les autres processeurs, à l’exception de celui actuel.

    Mais il est également possible, et recommandé par certains , d’obtenir des informations sur les processeurs via des structures de données spéciales configurées par le BIOS, telles que les tables ACPI ou le tableau de configuration MP d’Intel .

  5. XX en 000C46XXH code l’adresse de la première instruction que le processeur exécutera comme 000C46XXH :

     CS = XX * 0x100 IP = 0 

    Rappelez-vous que CS multiples adresses par 0x10 , l’adresse de mémoire réelle de la première instruction est donc:

     XX * 0x1000 

    Donc, si par exemple XX == 1 , le processeur démarrera à 0x1000 .

    Nous devons ensuite nous assurer que le code en mode réel 16 bits doit être exécuté à cet emplacement de mémoire, par exemple avec:

     cld mov $init_len, %ecx mov $init, %esi mov 0x1000, %edi rep movsb .code16 init: xor %ax, %ax mov %ax, %ds /* Do stuff. */ hlt .equ init_len, . - init 

    L’utilisation d’un script de l’éditeur de liens est une autre possibilité.

  6. Les boucles à retardement sont une tâche ennuyeuse à faire fonctionner: il n’y a pas de moyen très simple de faire ce type de sumil avec précision.

    Les méthodes possibles comprennent:

    • PIT (utilisé dans mon exemple)
    • HPET
    • calibrer le temps d’une boucle occupée avec ce qui précède, et l’utiliser à la place

    Related: Comment afficher un numéro à l’écran et dormir une seconde avec l’assemblage DOS x86?

  7. Je pense que le processeur initial doit être en mode protégé pour que cela fonctionne comme nous écrivons à l’adresse 0FEE00300H qui est trop élevée pour 16 bits

  8. Pour communiquer entre processeurs, nous pouvons utiliser un spinlock sur le processus principal et modifier le verrou du second cœur.

    Nous devons nous assurer que la wbinvd mémoire est effectuée, par exemple via wbinvd .

Etat partagé entre processeurs

8.7.1 “Etat des processeurs logiques” dit:

Les fonctionnalités suivantes font partie de l’état architectural des processeurs logiques des processeurs Intel 64 ou IA-32 prenant en charge la technologie Intel Hyper-Threading. Les fonctionnalités peuvent être subdivisées en trois groupes:

  • Dupliqué pour chaque processeur logique
  • Partagé par des processeurs logiques dans un processeur physique
  • Partagé ou dupliqué, selon l’implémentation

Les fonctionnalités suivantes sont dupliquées pour chaque processeur logique:

  • Registres à usage général (EAX, EBX, ECX, EDX, ESI, EDI, ESP et EBP)
  • Registres de segments (CS, DS, SS, ES, FS et GS)
  • Registres EFLAGS et EIP. Notez que les registres CS et EIP / RIP pour chaque processeur logique pointent vers le stream d’instructions pour le thread en cours d’exécution par le processeur logique.
  • Registres FPU x87 (ST0 à ST7, mot d’état, mot de contrôle, mot-clé, pointeur d’opérande de données et pointeur d’instruction)
  • Registres MMX (MM0 à MM7)
  • Registres XMM (XMM0 à XMM7) et le registre MXCSR
  • Registres de contrôle et registres de pointeurs de tables système (GDTR, LDTR, IDTR, registre de tâches)
  • Registres de débogage (DR0, DR1, DR2, DR3, DR6, DR7) et les MSR de contrôle de débogage
  • État global de la machine (IA32_MCG_STATUS) et capacité de vérification de la machine (IA32_MCG_CAP)
  • Modulation d’horloge thermique et ACR Contrôle de gestion de l’alimentation
  • Compteur horaire
  • La plupart des autres registres MSR, y compris la table d’atsortingbuts de page (PAT). Voir les exceptions ci-dessous.
  • Registres APIC locaux.
  • Registres à usage général supplémentaires (R8-R15), registres XMM (XMM8-XMM15), registre de contrôle, IA32_EFER sur les processeurs Intel 64.

Les fonctionnalités suivantes sont partagées par les processeurs logiques:

  • Registres de plage de type mémoire (MTRR)

Que les fonctionnalités suivantes soient partagées ou dupliquées est spécifique à l’implémentation:

  • IA32_MISC_ENABLE MSR (adresse MSR 1A0H)
  • MSR d’architecture MCA (sauf pour les MSR IA32_MCG_STATUS et IA32_MCG_CAP)
  • Contrôle de performance et compteurs MSR

Le partage de cache est discuté à:

Les hyperthreads Intel ont un plus grand partage de cache et de pipeline que les cœurs distincts: https://superuser.com/questions/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858

Noyau Linux 4.2

L’action d’initialisation principale semble être à l’ arch/x86/kernel/smpboot.c .

Exemples ARM

ARM semble être un peu plus facile à configurer que x86 car il a moins de temps d’historique, voici deux exemples minimaux d’exécution:

TODO: examinez ces exemples et expliquez-les mieux ici.

Ce document fournit des conseils sur l’utilisation des primitives de synchronisation ARM que vous pouvez ensuite utiliser pour effectuer des tâches amusantes avec plusieurs cœurs: http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitives.pdf

La FAQ non officielle de SMP logo de débordement de pile


Il était une fois, pour écrire l’assembleur x86, par exemple, que vous aviez des instructions indiquant “charger le registre EDX avec la valeur 5”, “incrémenter le registre EDX”, etc. Avec les CPU modernes qui ont 4 cœurs (ou plus) , au niveau du code de la machine, semble-t-il qu’il y a 4 processeurs distincts (c.-à-d. y a-t-il seulement 4 registres “EDX” distincts)?

Exactement. Il y a 4 jeux de registres, dont 4 pointeurs d’instruction distincts.

Si oui, quand vous dites “incrémenter le registre EDX”, qu’est-ce qui détermine quel registre EDX du processeur est incrémenté?

Le processeur qui a exécuté cette instruction, naturellement. Considérez-le comme 4 microprocesseurs complètement différents qui partagent simplement la même mémoire.

Existe-t-il un concept de “contexte CPU” ou de “thread” dans l’assembleur x86?

Non, l’assembleur ne fait que traduire les instructions comme il l’a toujours fait. Aucun changement là.

Comment fonctionne la communication / synchronisation entre les cœurs?

Comme ils partagent la même mémoire, c’est surtout une question de logique de programme. Bien qu’il existe maintenant un mécanisme d’ interruption entre processeurs , il n’est pas nécessaire et n’était pas présent à l’origine dans les premiers systèmes x86 à double processeur.

Si vous écriviez un système d’exploitation, quel mécanisme est exposé via le matériel pour vous permettre de planifier l’exécution sur différents cœurs?

Le planificateur ne change en fait pas, sauf qu’il s’agit d’un peu plus de précisions sur les sections critiques et les types de verrous utilisés. Avant SMP, le code du kernel appellerait éventuellement le planificateur, qui examinerait la queue d’exécution et choisirait un processus à exécuter comme thread suivant. (Les processus du kernel ressemblent beaucoup à des threads.) Le kernel SMP exécute exactement le même code, un thread à la fois, mais le locking de section critique doit désormais être sécurisé SMP pour être sûr que deux cœurs ne puissent pas le même PID.

Est-ce une instruction spéciale privilégiée?

Non. Les cœurs ne font que fonctionner dans la même mémoire avec les mêmes instructions.

Si vous écriviez une VM optimisée de compilateur / bytecode pour un processeur multicœur, que devriez-vous savoir spécifiquement sur, par exemple, x86 pour le faire générer du code qui s’exécute efficacement sur tous les cœurs?

Vous exécutez le même code qu’auparavant. C’est le kernel Unix ou Windows qui devait changer.

Vous pouvez résumer ma question en ces termes: “Quelles modifications ont été apscopes au code machine x86 pour prendre en charge les fonctionnalités multi-core?”

Rien n’était nécessaire. Les premiers systèmes SMP utilisaient exactement le même jeu d’instructions que les monoprocesseurs. Maintenant, il y a eu beaucoup d’évolution de l’architecture x86 et des milliers de nouvelles instructions pour accélérer les choses, mais aucune n’était nécessaire pour SMP.

Pour plus d’informations, consultez la spécification Intel Multiprocessor .


Mise à jour: on peut répondre à toutes les questions de suivi en acceptant simplement qu’un processeur multicœur n -way soit presque identique à un processeur séparé qui ne partage que la même mémoire. 2 Il y avait une question importante non posée: comment un programme écrit pour s’exécuter sur plus d’un cœur pour plus de performance? Et la réponse est: il est écrit en utilisant une bibliothèque de threads comme Pthreads. Certaines bibliothèques de threads utilisent des “threads verts” qui ne sont pas visibles par le système d’exploitation, et ceux-ci n’obtiendront pas de cœurs séparés, mais tant que la bibliothèque de threads utilisera les fonctionnalités de thread du kernel, votre programme sera automatiquement multicœur.


1. Pour la rétrocompatibilité, seul le premier cœur démarre à la réinitialisation, et quelques éléments de type pilote doivent être exécutés pour lancer les autres.
2. Ils partagent également tous les périphériques, naturellement.

Chaque Core s’exécute à partir d’une zone mémoire différente. Votre système d’exploitation indiquera un kernel à votre programme et le kernel exécutera votre programme. Votre programme ne saura pas qu’il y a plus d’un kernel ou sur lequel il est exécuté.

Il n’y a pas non plus d’instructions supplémentaires uniquement disponibles pour le système d’exploitation. Ces cœurs sont identiques aux puces à cœur unique. Chaque cœur exécute une partie du système d’exploitation qui gérera la communication avec les zones de mémoire communes utilisées pour l’échange d’informations afin de trouver la zone de mémoire suivante à exécuter.

C’est une simplification mais cela vous donne une idée de base de la manière dont cela se fait. En savoir plus sur les multicœurs et les multiprocesseurs sur Embedded.com a beaucoup d’informations sur ce sujet … Ce sujet se complique très vite!

Si vous écriviez une VM optimisée de compilateur / bytecode pour un processeur multicœur, que devriez-vous savoir spécifiquement sur, par exemple, x86 pour le faire générer du code qui s’exécute efficacement sur tous les cœurs?

En tant que personne qui écrit en optimisant des machines virtuelles compilateur / bytecode, je peux peut-être vous aider ici.

Vous n’avez pas besoin de savoir quelque chose de spécifique sur x86 pour le faire générer du code qui fonctionne efficacement sur tous les cœurs.

Cependant, vous aurez peut-être besoin de connaître cmpxchg et friends afin d’écrire du code qui s’exécute correctement sur tous les cœurs. La programmation multicœur nécessite l’utilisation de la synchronisation et de la communication entre les threads d’exécution.

Vous devrez peut-être savoir quelque chose à propos de x86 pour le faire générer du code qui fonctionne efficacement sur x86 en général.

Il y a d’autres choses qu’il vous serait utile d’apprendre:

Vous devez en savoir plus sur les fonctionnalités fournies par le système d’exploitation (Linux ou Windows ou OSX) pour vous permettre d’exécuter plusieurs threads. Vous devriez en apprendre davantage sur les API de parallélisation telles qu’OpenMP et les blocs de création de threads, ou OSX 10.6 “Snow Leopard”, le futur “Grand Central”.

Vous devriez vous demander si votre compilateur doit être auto-parallélisé ou si l’auteur des applications compilées par votre compilateur doit append une syntaxe spéciale ou des appels d’API dans son programme pour tirer parti des multiples cœurs.

Le code d’assemblage se traduira par un code machine qui sera exécuté sur un cœur. Si vous voulez qu’il soit multithreadé, vous devrez utiliser les primitives du système d’exploitation pour démarrer ce code sur différents processeurs plusieurs fois ou différents morceaux de code sur différents cœurs – chaque cœur exécutera un thread séparé. Chaque thread ne verra qu’un seul kernel sur lequel il s’exécute.

Ce n’est pas du tout dans les instructions de la machine; les cœurs prétendent être des processeurs distincts et ne disposent pas de capacités spéciales pour communiquer entre eux. Ils communiquent de deux manières:

  • ils partagent l’espace d’adressage physique. Le matériel gère la cohérence du cache, de sorte qu’un CPU écrit dans une adresse mémoire qu’un autre lit.

  • ils partagent un APIC (contrôleur d’interruption programmable). Il s’agit d’une mémoire mappée dans l’espace d’adresse physique et pouvant être utilisée par un processeur pour contrôler les autres, les activer ou les désactiver, envoyer des interruptions, etc.

http://www.cheesecake.org/sac/smp.html est une bonne référence avec une URL stupide.

La principale différence entre une application mono et une application multithread est que la première a une stack et la seconde une pour chaque thread. Le code est généré quelque peu différemment car le compilateur supposera que les registres de données et de segments de stack (ds et ss) ne sont pas égaux. Cela signifie que l’indirection par le biais des registres ebp et esp, par défaut au registre ss, ne sera pas non plus définie par défaut sur ds (car ds! = Ss). Inversement, l’indirection via les autres registres par défaut à ds ne sera pas par défaut à ss.

Les threads partagent tout le rest, y compris les zones de données et de code. Ils partagent également les routines lib alors assurez-vous qu’ils sont thread-safe. Une procédure qui sortinge une zone de la RAM peut être multi-thread pour accélérer les choses. Les threads accèderont, compareront et classeront les données dans la même zone de mémoire physique et exécuteront le même code mais en utilisant différentes variables locales pour contrôler leur partie respective du sorting. C’est bien sûr parce que les threads ont des stacks différentes contenant les variables locales. Ce type de programmation nécessite un réglage minutieux du code afin que les collisions de données inter-core (dans les caches et la RAM) soient réduites, ce qui entraîne un code plus rapide avec deux threads ou plus qu’avec un seul. Bien entendu, un code non réglé sera souvent plus rapide avec un processeur qu’avec deux ou plus. Déboguer est plus difficile car le point d’arrêt standard “int 3” ne sera pas applicable puisque vous voulez interrompre un thread spécifique et pas tous. Les points d’arrêt des registres de débogage ne résolvent pas non plus ce problème, sauf si vous pouvez les définir sur le processeur spécifique exécutant le thread spécifique que vous souhaitez interrompre.

Un autre code multi-thread peut impliquer différents threads exécutés dans différentes parties du programme. Ce type de programmation ne nécessite pas le même type de réglage et est donc beaucoup plus facile à apprendre.

Ce qui a été ajouté à chaque architecture à multitraitement par rapport aux variantes à processeur unique qui les ont précédés, ce sont des instructions pour la synchronisation entre les cœurs. De plus, vous avez des instructions pour gérer la cohérence du cache, les tampons de vidage et les opérations de bas niveau similaires auxquelles un système d’exploitation doit faire face. Dans le cas d’architectures multithread simultanées telles qu’IBM POWER6, IBM Cell, Sun Niagara et Intel “Hyperthreading”, vous avez également tendance à voir de nouvelles instructions pour hiérarchiser les threads (comme définir des priorités et céder explicitement le processeur lorsqu’il n’y a rien à faire) .

Mais la sémantique de base d’un seul thread est la même, il vous suffit d’append des fonctionnalités supplémentaires pour gérer la synchronisation et la communication avec les autres cœurs.