web-dev-qa-db-fra.com

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

Il était une fois, par exemple, pour écrire un assembleur x86, des instructions indiquant "chargez le registre EDX avec la valeur 5", "incrémentez le registre EDX", etc.

Avec les processeurs modernes à 4 cœurs (voire plus), au niveau du code machine, l’apparence est-elle identique à celle de 4 processeurs distincts (c’est-à-dire qu’il n’ya que 4 registres "EDX" distincts)? Si tel est le cas, lorsque vous dites "incrémenter le registre EDX", qu'est-ce qui détermine le registre EDX de la CPU qui est incrémenté? Existe-t-il actuellement un concept de "contexte de 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 privilégiée spéciale?

Si vous écriviez un compilateur/bytecode d'optimisation VM pour un processeur multicœur, que devez-vous savoir spécifiquement sur, par exemple, x86 pour le faire générer un code qui fonctionne efficacement sur tous les cœurs?

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

220
Paul Hollingsworth

Ce n'est pas une réponse directe à la question, mais c'est une réponse à une question qui apparaît dans les commentaires. Essentiellement, la question est de savoir quel support le matériel apporte au 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 le thread 0 dans le coeur 0 dans le processeur 0 ) commence à récupérer le 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 spéciale inter-processeur (IPI) via l'APIC appelée SIPI (Startup IPI) à chaque thread contenu dans WFS. Le SIPI contient l'adresse à partir de laquelle ce fil devrait commencer à chercher 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 d'un support logiciel pour chaque thread afin de configurer ses propres tables et files d'attente de messagerie. Le système d'exploitation utilise ceux-ci pour effectuer la planification multithread réelle.

En ce qui concerne l’assemblée proprement dite, comme l’a écrit Nicholas, il n’existe aucune différence entre les assemblées d’une application à thread unique ou multi-thread. Chaque thread logique a son propre ensemble de registres, écrivant ainsi:

mov edx, 0

mettra à jour uniquement 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 Assembly. Vous avez besoin d’une sorte d’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.

136
Nathan Fellman

Exemple de Baremetal minimal exécutable Intel x86

Exemple de métal nu pouvant être couru avec tous les standards requis . Toutes les parties principales sont couvertes ci-dessous.

Testé sur Ubuntu 15.10 QEMU 2.3.0 et Lenovo ThinkPad T400 invité matériel réel .

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

Tableau 8-1. "Séquence de diffusion INIT-SIPI-SIPI et choix du délai d'attente" contient un exemple qui ne fonctionne que:

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 noyau pour pouvoir l'utiliser librement: un programme Linux utilisateur ne fonctionnera pas.

  2. Au début, un seul processeur s'exécute, appelé le processeur bootstrap (BSP)].

    Il doit réveiller les autres (appelés Processeurs d’application (AP)) par des interruptions spéciales appelées Interrupteurs de processeur (IPI) .

    Ces interruptions peuvent être effectuées en programmant un contrôleur d'interruption programmable avancé (APIC) via le registre de commandes d'interruption (ICR).

    Le format de l'ICR est documenté à l'adresse suivante: 10.6 "Émission d'interruptions d'interprocesseur"

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

  3. ICR_LOW est défini à la section 8.4.4 "Exemple d'initialisation de MP" comme suit:

    ICR_LOW EQU 0FEE00300H
    

    La valeur magique 0FEE00300 est l'adresse de mémoire de l'ICR, comme indiqué dans le Tableau 10-1 "Carte d'adresses du registre APIC local".

  4. La méthode la plus simple possible est utilisée dans l'exemple: elle configure l'ICR pour envoyer des IPI de diffusion qui sont remis à tous les processeurs à l'exception du processeur 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 tables ACPI ou Intel MP = table de configuration et ne réveillez que ceux dont vous avez besoin un par un.

  5. XX dans 000C46XXH code l'adresse de la première instruction que le processeur exécutera sous la forme:

    CS = XX * 0x100
    IP = 0
    

    Rappelez-vous que CS adresses multiples par 0x10 , l’adresse 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 qu’un code en mode réel de 16 bits doit être exécuté à cet emplacement de la 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 de retard sont un élément ennuyeux pour se mettre au travail: il n’existe pas de moyen très simple de faire de tels dors avec précision.

    Les méthodes possibles incluent:

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

    Connexes: Comment afficher un nombre à l'écran et dormir pendant une seconde avec l'assemblage DOS x86?

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

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

    Nous devons nous assurer que la mémoire est réécrite, par exemple. à travers wbinvd.

Etat partagé entre processeurs

8.7.1 "État 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 les 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 de chaque processeur logique pointent vers le flux d'instructions du thread exécuté par le processeur logique.
  • registres FPU x87 (ST0 à ST7, mot d'état, mot de contrôle, mot d'étiquette, pointeur d'opérande de données et pointeur d'instruction)
  • Registres MMX (MM0 à MM7)
  • Les registres XMM (XMM0 à XMM7) et le registre MXCSR
  • Registres de contrôle et registres de pointeur de table 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 vérification de la machine (IA32_MCG_STATUS) et capacité de vérification de la machine (IA32_MCG_CAP)
  • Modulation d'horloge thermique et MSR de contrôle de gestion de l'alimentation ACPI
  • Compteur d'horodatage MSR
  • La plupart des autres registres MSR, y compris la table d'attributs de page (PAT). Voir les exceptions ci-dessous.
  • Registres APIC locaux.
  • Registres polyvalents supplémentaires (R8-R15), registres XMM (XMM8-XMM15), registre de contrôle, IA32_EFER sur processeurs Intel 64.

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

  • Registres de plage de type de 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 de MCA (architecture de contrôle machine) (à l'exception des MSR IA32_MCG_STATUS et IA32_MCG_CAP)
  • Contrôle de la performance et contrôle des MSR

Le partage de cache est discuté à:

Les hyperthreads d'Intel permettent un plus grand partage du cache et du pipeline que des cœurs séparés: 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 à Arch/x86/kernel/smpboot.c.

Exemple de baremetal minimal exécutable ARM

Ici, je fournis un exemple minimal exécutable ARMv8 aarch64 pour QEMU:

.global mystart
mystart:
    /* Reset spinlock. */
    mov x0, #0
    ldr x1, =spinlock
    str x0, [x1]

    /* Read cpu id into x1.
     * TODO: cores beyond 4th?
     * Mnemonic: Main Processor ID Register
     */
    mrs x1, mpidr_el1
    ands x1, x1, 3
    beq cpu0_only
cpu1_only:
    /* Only CPU 1 reaches this point and sets the spinlock. */
    mov x0, 1
    ldr x1, =spinlock
    str x0, [x1]
    /* Ensure that CPU 0 sees the write right now.
     * Optional, but could save some useless CPU 1 loops.
     */
    dmb sy
    /* Wake up CPU 0 if it is sleeping on wfe.
     * Optional, but could save power on a real system.
     */
    sev
cpu1_sleep_forever:
    /* Hint CPU 1 to enter low power mode.
     * Optional, but could save power on a real system.
     */
    wfe
    b cpu1_sleep_forever
cpu0_only:
    /* Only CPU 0 reaches this point. */

    /* Wake up CPU 1 from initial sleep!
     * See:https://github.com/cirosantilli/linux-kernel-module-cheat#psci
     */
    /* PCSI function identifier: CPU_ON. */
    ldr w0, =0xc4000003
    /* Argument 1: target_cpu */
    mov x1, 1
    /* Argument 2: entry_point_address */
    ldr x2, =cpu1_only
    /* Argument 3: context_id */
    mov x3, 0
    /* Unused hvc args: the Linux kernel zeroes them,
     * but I don't think it is required.
     */
    hvc 0

spinlock_start:
    ldr x0, spinlock
    /* Hint CPU 0 to enter low power mode. */
    wfe
    cbz x0, spinlock_start

    /* Semihost exit. */
    mov x1, 0x26
    movk x1, 2, lsl 16
    str x1, [sp, 0]
    mov x0, 0
    str x0, [sp, 8]
    mov x1, sp
    mov w0, 0x18
    hlt 0xf000

spinlock:
    .skip 8

GitHub en amont .

Assemblez et courez:

aarch64-linux-gnu-gcc \
  -mcpu=cortex-a57 \
  -nostdlib \
  -nostartfiles \
  -Wl,--section-start=.text=0x40000000 \
  -Wl,-N \
  -o aarch64.elf \
  -T link.ld \
  aarch64.S \
;
qemu-system-aarch64 \
  -machine virt \
  -cpu cortex-a57 \
  -d in_asm \
  -kernel aarch64.elf \
  -nographic \
  -semihosting \
  -smp 2 \
;

Dans cet exemple, nous plaçons la CPU 0 dans une boucle de spinlock et elle n'existe que lorsque la CPU 1 libère le spinlock.

Après le spinlock, la CPU 0 effectue alors un appel de sortie semi-hôte qui oblige QEMU à quitter.

Si vous démarrez QEMU avec un seul processeur avec -smp 1, alors la simulation est bloquée pour toujours sur le spinlock.

La CPU 1 est réveillée avec l'interface PSCI, plus de détails à: ARM: Démarrer/Réveil/Amener les autres cœurs de processeur/AP et transmettre l'adresse de début d'exécution?

La version version en amont comporte également quelques modifications pour que cela fonctionne avec gem5, vous pouvez ainsi expérimenter les caractéristiques de performance.

Je ne l'ai pas testé sur du matériel réel, donc je ne suis pas sûr que ce soit portable. La bibliographie suivante sur Raspberry Pi pourrait être intéressante:

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

Testé sur Ubuntu 18.10, GCC 8.2.0, Binutils 2.31.1, QEMU 2.12.0.

Prochaines étapes pour une programmabilité plus pratique

Les exemples précédents réveillent la CPU secondaire et effectuent la synchronisation de base de la mémoire avec des instructions dédiées, ce qui est un bon début.

Mais pour rendre les systèmes multicœurs faciles à programmer, par exemple comme POSIX pthreads, vous devrez également entrer dans les sujets plus complexes suivants:

  • le programme d'installation interrompt et exécute un minuteur qui détermine périodiquement quel thread sera exécuté maintenant. C'est ce qu'on appelle multithreading préemptif .

    Un tel système doit également sauvegarder et restaurer les registres de threads à mesure qu'ils sont démarrés et arrêtés.

    Il est également possible d’avoir des systèmes multitâches non préemptifs, mais ceux-ci peuvent vous obliger à modifier votre code afin que chaque thread donne des résultats (par exemple avec un pthread_yield mise en œuvre) et il devient plus difficile d’équilibrer la charge de travail.

    Voici quelques exemples simplistes de minuteur nu-métal:

  • gérer les conflits de mémoire. Notamment, chaque thread nécessitera un pile unique .

    Vous pouvez simplement limiter les threads à une taille de pile maximale fixe, mais le moyen le plus agréable de procéder consiste à utiliser paging , ce qui permet de disposer de piles efficaces de "taille illimitée".

Voilà quelques bonnes raisons d'utiliser le noyau Linux ou un autre système d'exploitation :-)

Primitives de synchronisation de la mémoire Userland

Bien que le démarrage/arrêt/la gestion des threads dépasse généralement la portée de l'utilisateur, vous pouvez toutefois utiliser les instructions d'assemblage provenant des threads utilisateur pour synchroniser les accès à la mémoire sans appels système potentiellement plus coûteux.

Vous devriez bien sûr préférer utiliser des bibliothèques qui encapsulent de manière portable ces primitives de bas niveau. La norme C++ elle-même a fait de grands progrès sur le <atomic> en-tête, et en particulier avec std::memory_order . Je ne sais pas si cela couvre toutes les sémantiques possibles de la mémoire, mais ça se pourrait.

Les sémantiques plus subtiles sont particulièrement pertinentes dans le contexte de structures de données sans verrouillage , qui peuvent offrir des avantages de performance dans certains cas. Pour les implémenter, vous devrez probablement vous familiariser avec les différents types de barrières de mémoire: https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/ =

Boost, par exemple, a des implémentations de conteneur sans verrouillage à l’adresse suivante: https://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html

Voici un exemple minimal inutilisé C++ x86_64/aarch64 avec un assemblage en ligne qui illustre l'utilisation de base de telles instructions, principalement pour le plaisir:

main.cpp

#include <atomic>
#include <cassert>
#include <iostream>
#include <thread>
#include <vector>

std::atomic_ulong my_atomic_ulong(0);
unsigned long my_non_atomic_ulong = 0;
#if defined(__x86_64__) || defined(__aarch64__)
unsigned long my_Arch_atomic_ulong = 0;
unsigned long my_Arch_non_atomic_ulong = 0;
#endif
size_t niters;

void threadMain() {
    for (size_t i = 0; i < niters; ++i) {
        my_atomic_ulong++;
        my_non_atomic_ulong++;
#if defined(__x86_64__)
        __asm__ __volatile__ (
            "incq %0;"
            : "+m" (my_Arch_non_atomic_ulong)
            :
            :
        );
        // https://github.com/cirosantilli/linux-kernel-module-cheat#x86-lock-prefix
        __asm__ __volatile__ (
            "lock;"
            "incq %0;"
            : "+m" (my_Arch_atomic_ulong)
            :
            :
        );
#Elif defined(__aarch64__)
        __asm__ __volatile__ (
            "add %0, %0, 1;"
            : "+r" (my_Arch_non_atomic_ulong)
            :
            :
        );
        // https://github.com/cirosantilli/linux-kernel-module-cheat#arm-lse
        __asm__ __volatile__ (
            "ldadd %[inc], xzr, [%[addr]];"
            : "=m" (my_Arch_atomic_ulong)
            : [inc] "r" (1),
              [addr] "r" (&my_Arch_atomic_ulong)
            :
        );
#endif
    }
}

int main(int argc, char **argv) {
    size_t nthreads;
    if (argc > 1) {
        nthreads = std::stoull(argv[1], NULL, 0);
    } else {
        nthreads = 2;
    }
    if (argc > 2) {
        niters = std::stoull(argv[2], NULL, 0);
    } else {
        niters = 10000;
    }
    std::vector<std::thread> threads(nthreads);
    for (size_t i = 0; i < nthreads; ++i)
        threads[i] = std::thread(threadMain);
    for (size_t i = 0; i < nthreads; ++i)
        threads[i].join();
    assert(my_atomic_ulong.load() == nthreads * niters);
    // We can also use the atomics direclty through `operator T` conversion.
    assert(my_atomic_ulong == my_atomic_ulong.load());
    std::cout << "my_non_atomic_ulong " << my_non_atomic_ulong << std::endl;
#if defined(__x86_64__) || defined(__aarch64__)
    assert(my_Arch_atomic_ulong == nthreads * niters);
    std::cout << "my_Arch_non_atomic_ulong " << my_Arch_non_atomic_ulong << std::endl;
#endif
}

GitHub en amont .

Sortie possible:

my_non_atomic_ulong 15264
my_Arch_non_atomic_ulong 15267

Nous voyons de là que l’instruction préfixe x86 LOCK/aarch64 LDADD a rendu l’ajout atomique: sans elle, nous avons des conditions de concurrence sur de nombreux ajouts, et le nombre total à la fin est inférieur à 20000 synchronisé.

Voir aussi: Que signifie l'instruction "lock" dans l'assemblage x86?

Testé sous Ubuntu 19.04 AMD64 et en mode utilisateur QEMU aarch64.

Si je comprends bien, chaque "cœur" est un processeur complet, avec son propre jeu de registres. Fondamentalement, le BIOS vous lance avec un seul cœur en cours d'exécution, puis le système d'exploitation peut "démarrer" d'autres noyaux en les initialisant et en les pointant 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. La fonctionnalité multitâche du système d'exploitation est donc chargée de décider quel processus doit toucher quelle mémoire et quoi faire en cas de collision de mémoire.

42
Nicholas Flynt

Chaque Core s'exécute à partir d'une zone de mémoire différente. Votre système d'exploitation dirigera un noyau vers votre programme et le noyau exécutera votre programme. Votre programme ne saura pas qu'il y a plus d'un noyau ou sur quel noyau il s'exécute.

De plus, aucune instruction supplémentaire n'est disponible pour le système d'exploitation. Ces cœurs sont identiques aux puces à un cœur. 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 rechercher la prochaine zone de mémoire à exécuter.

C'est une simplification, mais cela vous donne une idée de base de la façon dont cela est fait. Plus d'informations sur les multicœurs et les multiprocesseurs sur Embedded.com contient de nombreuses informations sur ce sujet ... Ce sujet se complique très rapidement!

9
Gerhard

Si vous écriviez un compilateur/bytecode d'optimisation VM pour un processeur multicœur, que devez-vous savoir spécifiquement sur, par exemple, x86 pour le faire générer un code qui fonctionne efficacement sur tous les cœurs?

En tant que rédacteur optimisant des ordinateurs virtuels compilateur/bytecode, je peux peut-être vous aider.

Vous n'avez besoin de rien savoir spécifiquement sur x86 pour le faire générer un code qui fonctionne efficacement sur tous les cœurs.

Cependant, vous devrez peut-être connaître cmpxchg and friends pour pouvoir é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 connaître quelque chose sur x86 pour lui permettre de générer un code qui fonctionne efficacement sur x86 en général.

Il serait utile que vous appreniez d'autres choses:

Vous devriez vous renseigner sur les fonctionnalités du système d'exploitation (Linux, Windows ou OSX) pour vous permettre d'exécuter plusieurs threads. Vous devriez en savoir plus sur les API de parallélisation telles que OpenMP et les blocs de construction de threading, ou le prochain "Grand Central" d'OSX 10.6 "Snow Leopard".

Vous devez déterminer si votre compilateur doit effectuer une mise en parallèle automatique ou si l'auteur des applications compilées par votre compilateur doit ajouter une syntaxe spéciale ou des appels d'API à son programme pour tirer parti des multiples cœurs.

9
Alex Brown

Le code d'assemblage sera traduit en code machine qui sera exécuté sur un noyau. Si vous voulez qu'il soit multithread, vous devrez utiliser des primitives du système d'exploitation pour lancer ce code sur plusieurs processeurs à plusieurs reprises ou différents morceaux de code sur des cœurs différents - chaque core exécutera un thread séparé. Chaque thread ne verra qu'un seul noyau sur lequel il est en train de s'exécuter.

5
sharptooth

Cela ne se fait 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 de la mémoire cache. Un processeur écrit sur une adresse mémoire lue par une autre.

  • ils partagent un APIC (contrôleur d'interruption programmable). Cette mémoire est mappée dans l'espace d'adressage physique et peut ê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 idiote.

3
pjc50

La principale différence entre une application à un et plusieurs applications réside dans le fait que la première a une pile et que la dernière en a une pour chaque thread. Le code est généré quelque peu différemment puisque le compilateur supposera que les registres de données et de segments de pile (ds et ss) ne sont pas égaux. Cela signifie que les indications indirectionnelles via les registres ebp et esp qui sont définies par défaut sur le registre ss ne sont pas également définies sur ds (car ds! = Ss). Inversement, l'indirection par les autres registres, qui par défaut est ds, ne sera pas par défaut, ss.

Les threads partagent tout le reste, y compris les zones de données et de code. Ils partagent également les routines de la bibliothèque, assurez-vous qu’elles sont thread-safe. Une procédure qui trie une zone dans RAM peut être multithread pour accélérer les choses. Les threads accéderont, compareront et ordonneront les données dans la même zone de mémoire physique et exécuteront le même code mais utiliser différentes variables locales pour contrôler leurs parties respectives du tri, ce qui est bien sûr dû au fait que les threads ont des piles différentes dans lesquelles les variables locales sont contenues. Ce type de programmation nécessite un réglage minutieux du code afin que les collisions de données inter-coeurs et RAM) sont réduits, ce qui entraîne un code plus rapide avec deux threads ou plus que avec un seul, bien sûr, un code non ajusté sera souvent plus rapide avec un processeur qu'avec deux ou plus. est plus difficile car le point d'arrêt standard "int 3" ne sera pas applicable car vous souhaitez interrompre un thread spécifique et non tous. Les points d'arrêt du registre de débogage ne résolvent pas ce problème non plus, à moins que vous ne puissiez les définir sur le processeur fil ecific que vous voulez interrompre.

Un autre code multithread peut impliquer différents threads s'exécutant dans différentes parties du programme. Ce type de programmation ne nécessite pas le même type d’accord et est donc beaucoup plus facile à apprendre.

1
Olof Forshell

Ce qui a été ajouté à chaque architecture compatible avec le multitraitement par rapport aux variantes à processeur unique qui les ont précédées est une instruction de synchronisation entre les cœurs. En outre, vous avez des instructions pour gérer la cohérence du cache, le vidage des mémoires tampons et les opérations de bas niveau similaires auxquelles un système d'exploitation doit faire face. "Hyperthreading" d'IBM POWER6, d'IBM Cell, de Sun Niagara et d'Intel simultanés dans les architectures multithreads simultanées, vous avez également tendance à voir de nouvelles instructions pour hiérarchiser les threads (comme la définition de priorités et la cession explicite du 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'ajouter des fonctionnalités supplémentaires pour gérer la synchronisation et la communication avec d'autres cœurs.

0
jakobengblom2