wake-up-neo.com

Wie sieht die Multicore-Assemblersprache aus?

Wenn Sie beispielsweise einen x86-Assembler schreiben möchten, müssen Sie die Anweisungen "Laden des EDX-Registers mit dem Wert 5", "Inkrementieren des EDX-Registers" usw. ausführen.

Bei modernen CPUs mit 4 Kernen (oder mehr) sieht es auf Maschinencodeebene so aus, als gäbe es 4 separate CPUs (d. H. Gibt es nur 4 verschiedene "EDX" -Register)? Wenn ja, wenn Sie "Inkrementieren des EDX-Registers" sagen, was bestimmt, welches EDX-Register der CPU inkrementiert wird? Gibt es im x86-Assembler jetzt ein Konzept für "CPU-Kontext" oder "Thread"?

Wie funktioniert die Kommunikation/Synchronisation zwischen den Kernen?

Wenn Sie ein Betriebssystem geschrieben haben, welcher Mechanismus wird über Hardware verfügbar gemacht, damit Sie die Ausführung auf verschiedenen Kernen planen können? Handelt es sich um spezielle privilegierte Anweisungen?

Wenn Sie einen optimierenden Compiler/Bytecode VM für eine Multicore-CPU schreiben, was müssen Sie dann speziell über x86 wissen, damit dieser Code effizient auf allen Kernen ausgeführt wird?

Welche Änderungen wurden am x86-Computercode vorgenommen, um die Multi-Core-Funktionalität zu unterstützen?

220

Dies ist keine direkte Antwort auf die Frage, sondern eine Antwort auf eine Frage, die in den Kommentaren angezeigt wird. Im Wesentlichen stellt sich die Frage, welche Unterstützung die Hardware für den Multithread-Betrieb bietet.

Nicholas Flynt hatte es richtig , zumindest in Bezug auf x86. In einer Umgebung mit mehreren Threads (Hyper-Threading, Multi-Core oder Multi-Prozessor) ist der Bootstrap-Thread (normalerweise Thread 0 in Core 0 in Prozessor 0) ) startet das Abrufen des Codes von der Adresse 0xfffffff0. Alle anderen Threads werden in einem speziellen Ruhezustand gestartet, der als Warten auf SIPI bezeichnet wird. Im Rahmen seiner Initialisierung sendet der primäre Thread über den APIC einen speziellen Interprozessor-Interrupt (IPI), der als SIPI (Startup IPI) bezeichnet wird, an jeden Thread in WFS. Die SIPI enthält die Adresse, von der der Thread den Code abrufen soll.

Dieser Mechanismus ermöglicht es jedem Thread, Code von einer anderen Adresse auszuführen. Alles, was benötigt wird, ist Software-Unterstützung für jeden Thread, um seine eigenen Tabellen und Messaging-Warteschlangen einzurichten. Das Betriebssystem verwendet diese , um die eigentliche Multithread-Planung durchzuführen.

In Bezug auf die eigentliche Assembly gibt es, wie Nicholas schrieb, keinen Unterschied zwischen den Assemblys für eine Single-Thread- oder Multi-Thread-Anwendung. Jeder logische Thread hat einen eigenen Registersatz. Schreiben Sie also:

mov edx, 0

wird nur EDX für den aktuell laufenden Thread aktualisieren. Es gibt keine Möglichkeit, EDX auf einem anderen Prozessor mit einer einzelnen Assembly-Anweisung zu ändern. Sie benötigen eine Art Systemaufruf, um das Betriebssystem aufzufordern, einem anderen Thread mitzuteilen, Code auszuführen, der sein eigenes EDX aktualisiert.

136
Nathan Fellman

Intel x86 Minimal Runable Baremetal Beispiel

Lauffähiges Bare-Metal-Beispiel mit allen erforderlichen Kesselschildern . Alle Hauptteile werden unten behandelt.

Getestet auf Ubuntu 15.10 QEMU 2.3.0 und Lenovo ThinkPad T400 als echter Hardware-Gast .

Das Handbuch zur Systemprogrammierung für Intel Manual Volume 3 - 325384-056DE September 2015 behandelt SMP in den Kapiteln 8, 9 und 10.

Tabelle 8-1. "Broadcast INIT-SIPI-SIPI-Sequenz und Auswahl von Timeouts" enthält ein Beispiel, das im Grunde nur funktioniert:

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

Auf diesem Code:

  1. Die meisten Betriebssysteme machen die meisten dieser Vorgänge über Ring 3 (Benutzerprogramme) unmöglich.

    Sie müssen also Ihren eigenen Kernel schreiben, um frei damit spielen zu können: Ein userland Linux-Programm wird nicht funktionieren.

  2. Zunächst wird ein einzelner Prozessor ausgeführt, der bootstrap Prozessor (BSP)) genannt wird.

    Es muss die anderen (als Application Processors (AP) bezeichnet) durch spezielle Interrupts Inter Processor Interrupts (IPI) aufwecken.

    Diese Interrupts können durch Programmieren des Advanced Programmable Interrupt Controller (APIC) über das Interrupt Command Register (ICR) ausgeführt werden.

    Das Format des ICR ist dokumentiert unter: 10.6 "ISSUING INTERPROCESSOR INTERRUPTS"

    Die IPI erfolgt, sobald wir an das ICR schreiben.

  3. ICR_LOW ist in 8.4.4 "MP Initialization Example" definiert als:

    ICR_LOW EQU 0FEE00300H
    

    Der magische Wert 0FEE00300 Ist die Speicheradresse des ICR, wie in Tabelle 10-1 "Local APIC Register Address Map" dokumentiert.

  4. Im Beispiel wird die einfachste Methode verwendet: Sie richtet den ICR so ein, dass Broadcast-IPIs gesendet werden, die an alle anderen Prozessoren mit Ausnahme des aktuellen Prozessoren gesendet werden.

    Es ist aber auch möglich und von einigen empfohlen , Informationen über die Prozessoren durch spezielle Datenstrukturen zu erhalten, die vom BIOS eingerichtet wurden, wie z. B. ACPI-Tabellen oder Intels MP Konfigurationstabelle und wecken Sie nur die, die Sie nacheinander benötigen.

  5. XX in 000C46XXH codiert die Adresse des ersten Befehls, den der Prozessor ausführt, als:

    CS = XX * 0x100
    IP = 0
    

    Denken Sie daran, dass CS Adressen mit 0x10 multipliziert, sodass die tatsächliche Speicheradresse des ersten Befehls wie folgt lautet:

    XX * 0x1000
    

    Wenn also zum Beispiel XX == 1, Startet der Prozessor bei 0x1000.

    Wir müssen dann sicherstellen, dass es einen 16-Bit-Real-Mode-Code gibt, der an diesem Speicherort ausgeführt werden soll, z. mit:

    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
    

    Die Verwendung eines Linker-Skripts ist eine weitere Möglichkeit.

  6. Die Verzögerungsschleifen sind ein ärgerlicher Teil, um arbeiten zu können: Es gibt keine super einfache Möglichkeit, solche Schlafvorgänge präzise durchzuführen.

    Mögliche Methoden sind:

    • PIT (in meinem Beispiel verwendet)
    • HPET
    • kalibrieren Sie die Zeit einer Besetztschleife mit den obigen Angaben und verwenden Sie sie stattdessen

    Verwandte Themen: Wie kann ich eine Zahl auf dem Bildschirm anzeigen und mit DOS x86-Assembly eine Sekunde lang in den Energiesparmodus wechseln?

  7. Ich denke, der ursprüngliche Prozessor muss im geschützten Modus sein, damit dies funktioniert, wenn wir an die Adresse 0FEE00300H Schreiben, die für 16-Bit zu hoch ist

  8. Um zwischen Prozessoren zu kommunizieren, können wir einen Spinlock für den Hauptprozess verwenden und die Sperre vom zweiten Kern aus ändern.

    Wir sollten sicherstellen, dass das Zurückschreiben des Speichers erfolgt, z. durch wbinvd.

Gemeinsamer Status zwischen Prozessoren

8.7.1 "Status der logischen Prozessoren" lautet:

Die folgenden Funktionen sind Teil des Architekturzustands logischer Prozessoren in Intel 64- oder IA-32-Prozessoren, die die Intel Hyper-Threading-Technologie unterstützen. Die Funktionen können in drei Gruppen unterteilt werden:

  • Für jeden logischen Prozessor dupliziert
  • Wird von logischen Prozessoren in einem physischen Prozessor gemeinsam genutzt
  • Geteilt oder dupliziert, je nach Implementierung

Die folgenden Funktionen werden für jeden logischen Prozessor dupliziert:

  • Allzweckregister (EAX, EBX, ECX, EDX, ESI, EDI, ESP und EBP)
  • Segmentregister (CS, DS, SS, ES, FS und GS)
  • EFLAGS- und EIP-Register. Beachten Sie, dass die CS- und EIP/RIP-Register für jeden logischen Prozessor auf den Befehlsstrom für den Thread zeigen, der vom logischen Prozessor ausgeführt wird.
  • x87-FPU-Register (ST0 bis ST7, Statuswort, Steuerwort, Tag-Wort, Datenoperandenzeiger und Befehlszeiger)
  • MMX-Register (MM0 bis MM7)
  • XMM-Register (XMM0 bis XMM7) und das MXCSR-Register
  • Steuerregister und Systemtabellenzeigerregister (GDTR, LDTR, IDTR, Taskregister)
  • Debug-Register (DR0, DR1, DR2, DR3, DR6, DR7) und die Debug-Steuerungs-MSRs
  • MSRs für den globalen Status der Maschinenprüfung (IA32_MCG_STATUS) und für die Maschinenprüfung (IA32_MCG_CAP)
  • Thermische Taktmodulation und ACPI Power Management steuern MSRs
  • Zeitstempelzähler MSRs
  • Die meisten anderen MSR-Register, einschließlich der Seitenattributtabelle (PAT). Siehe die folgenden Ausnahmen.
  • Lokale APIC-Register.
  • Zusätzliche Allzweckregister (R8-R15), XMM-Register (XMM8-XMM15), Steuerregister, IA32_EFER bei Intel 64-Prozessoren.

Die folgenden Funktionen werden von logischen Prozessoren gemeinsam genutzt:

  • Speichertyp-Bereichsregister (MTRRs)

Ob die folgenden Funktionen gemeinsam genutzt oder dupliziert werden, hängt von der jeweiligen Implementierung ab:

  • IA32_MISC_ENABLE MSR (MSR-Adresse 1A0H)
  • MSRs der Machine Check Architecture (MCA) (mit Ausnahme der MSRs IA32_MCG_STATUS und IA32_MCG_CAP)
  • Leistungsüberwachungssteuerung und Zähler-MSRs

Cache-Sharing wird diskutiert unter:

Intel-Hyperthreads haben eine größere gemeinsame Cache- und Pipeline-Nutzung als separate Kerne: https://superuser.com/questions/133082/hyper-threading-and-dual-core-whats-the-difference/995858 # 995858

Linux-Kernel 4.2

Die Hauptinitialisierungsaktion scheint bei Arch/x86/kernel/smpboot.c Zu sein.

ARM Minimal Runable Baremetal Beispiel

Hier ein minimal lauffähiges ARMv8 aarch64 Beispiel für 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 upstream .

Zusammenbauen und ausführen:

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 \
;

In diesem Beispiel setzen wir die CPU 0 in eine Spinlock-Schleife und beenden sie erst, wenn die CPU 1 den Spinlock freigibt.

Nach dem Spinlock führt die CPU 0 dann einen Semihost-Exit-Aufruf aus, der QEMU beendet.

Wenn Sie QEMU mit nur einer CPU mit -smp 1 Starten, hängt die Simulation für immer am Spinlock.

CPU 1 wird mit der PSCI-Schnittstelle aufgeweckt. Weitere Informationen finden Sie unter: ARM: Start/Wakeup/Bringup der anderen CPU-Kerne/APs und Übergabe der Startadresse für die Ausführung?

Die Upstream-Version hat auch ein paar Verbesserungen, damit sie auf gem5 funktioniert, sodass Sie auch mit den Leistungsmerkmalen experimentieren können.

Ich habe es nicht auf echter Hardware getestet und bin mir nicht sicher, wie portabel das ist. Die folgende Raspberry Pi-Bibliographie könnte Sie interessieren:

Dieses Dokument enthält einige Anleitungen zur Verwendung von ARM Synchronisationsprimitiven, mit denen Sie dann unterhaltsame Dinge mit mehreren Kernen ausführen können: http://infocenter.arm.com /help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitives.pdf

Getestet unter Ubuntu 18.10, GCC 8.2.0, Binutils 2.31.1, QEMU 2.12.0.

Nächste Schritte für eine bequemere Programmierbarkeit

In den vorherigen Beispielen wird die sekundäre CPU aktiviert und die Hauptspeichersynchronisierung mit dedizierten Anweisungen ausgeführt. Dies ist ein guter Anfang.

Um die Programmierung von Multicore-Systemen zu vereinfachen, z. Wie [~ # ~] posix [~ # ~]pthreads müssten Sie auch auf die folgenden komplexeren Themen eingehen:

  • setup unterbricht und führt einen Timer aus, der in regelmäßigen Abständen entscheidet, welcher Thread jetzt ausgeführt wird. Dies wird als preemptives Multithreading bezeichnet.

    Ein solches System muss auch Thread-Register speichern und wiederherstellen, wenn sie gestartet und gestoppt werden.

    Es ist auch möglich, nicht-präemptive Multitasking-Systeme zu verwenden. Möglicherweise müssen Sie jedoch Ihren Code so ändern, dass alle Threads (z. B. mit einer pthread_yield - Implementierung) ein Ergebnis erzielen, und es wird schwieriger, die Arbeitslast auszugleichen.

    Hier sind einige vereinfachte Bare-Metal-Timer-Beispiele:

  • mit Gedächtniskonflikten umgehen. Insbesondere benötigt jeder Thread einen eindeutigen Stapel .

    Sie könnten Threads auf eine feste maximale Stapelgröße beschränken, aber der schönere Weg, damit umzugehen, ist das Paging , das eine effiziente "unbegrenzte Größe" ermöglicht. Stapel.

Das sind einige gute Gründe, den Linux-Kernel oder ein anderes Betriebssystem zu verwenden :-)

Grundelemente für die Userland-Speichersynchronisierung

Obwohl das Starten/Stoppen/Verwalten von Threads in der Regel außerhalb des Bereichs von Userland liegt, können Sie Assembly-Anweisungen von Userland-Threads verwenden, um Speicherzugriffe ohne potenziell teurere Systemaufrufe zu synchronisieren.

Sie sollten es natürlich vorziehen, Bibliotheken zu verwenden, die diese einfachen Grundelemente portabel umschließen. Der C++ - Standard selbst hat große Fortschritte beim <atomic> - Header gemacht, insbesondere mit std::memory_order . Ich bin nicht sicher, ob es alle möglichen erreichbaren Speichersemantiken abdeckt, aber es könnte sein.

Die subtilere Semantik ist insbesondere im Kontext von sperrfreien Datenstrukturen relevant, die in bestimmten Fällen Leistungsvorteile bieten können. Um diese zu implementieren, müssen Sie wahrscheinlich etwas über die verschiedenen Arten von Speicherbarrieren lernen: https://preshing.com/20120710/memory-barriers-are-like-source-control -Operationen/

Boost hat beispielsweise einige sperrenfreie Containerimplementierungen unter: https://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html

Hier ist ein minimal nutzloses C++ x86_64/aarch64-Beispiel mit Inline-Assembly, das die grundlegende Verwendung solcher Anweisungen hauptsächlich zum Spaß veranschaulicht:

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 upstream .

Mögliche Ausgabe:

my_non_atomic_ulong 15264
my_Arch_non_atomic_ulong 15267

Daraus geht hervor, dass die Anweisung x86 LOCK prefix/aarch64 LDADD die Addition atomar gemacht hat: Ohne sie haben wir auf vielen Additionen Race-Bedingungen, und die Gesamtanzahl am Ende ist geringer als die synchronisierte 20000.

Siehe auch: Was bedeutet die Anweisung "lock" in der x86-Assembly?

Getestet in Ubuntu 19.04 AMD64 und mit QEMU aarch64 User Mode.

Nach meinem Verständnis ist jeder "Kern" ein vollständiger Prozessor mit einem eigenen Registersatz. Grundsätzlich startet Sie das BIOS mit einem ausgeführten Kern, und dann kann das Betriebssystem andere Kerne "starten", indem es sie initialisiert und auf den auszuführenden Code verweist.

Die Synchronisation erfolgt über das Betriebssystem. Im Allgemeinen wird auf jedem Prozessor ein anderer Prozess für das Betriebssystem ausgeführt. Die Multithreading-Funktionalität des Betriebssystems entscheidet, welcher Prozess welchen Speicher berührt und was im Falle einer Speicherkollision zu tun ist.

42
Nicholas Flynt

Jeder Core wird aus einem anderen Speicherbereich ausgeführt. Ihr Betriebssystem zeigt einen Kern auf Ihr Programm und der Kern führt Ihr Programm aus. Ihr Programm wird nicht wissen, dass es mehr als einen Kern gibt oder auf welchem ​​Kern es ausgeführt wird.

Es gibt auch keine zusätzlichen Anweisungen, die nur für das Betriebssystem verfügbar sind. Diese Kerne sind mit Einkernchips identisch. Auf jedem Core wird ein Teil des Betriebssystems ausgeführt, der die Kommunikation mit gemeinsamen Speicherbereichen für den Informationsaustausch abwickelt, um den nächsten auszuführenden Speicherbereich zu finden.

Dies ist eine Vereinfachung, gibt Ihnen aber eine grundlegende Vorstellung davon, wie es gemacht wird. Weitere Informationen zu Multicores und Multiprozessoren auf Embedded.com enthält viele Informationen zu diesem Thema ... Dieses Thema wird sehr schnell kompliziert!

9
Gerhard

Wenn Sie einen optimierenden Compiler/Bytecode VM für eine Multicore-CPU schreiben, was müssen Sie dann speziell über x86 wissen, damit dieser Code effizient auf allen Kernen ausgeführt wird?

Als jemand, der optimierende Compiler-/Bytecode-VMs schreibt, kann ich Ihnen möglicherweise hier weiterhelfen.

Sie müssen nichts spezielles über x86 wissen, damit es Code generiert, der effizient auf allen Kernen ausgeführt wird.

Möglicherweise müssen Sie jedoch mit cmpxchg und Freunden vertraut sein, um Code zu schreiben, der korrekt auf allen Kernen ausgeführt wird. Multicore-Programmierung erfordert die Verwendung von Synchronisation und Kommunikation zwischen Ausführungsthreads.

Möglicherweise müssen Sie etwas über x86 wissen, damit Code generiert wird, der auf x86 im Allgemeinen effizient ausgeführt wird.

Es gibt noch andere Dinge, die Sie lernen sollten:

Sie sollten sich mit den Funktionen des Betriebssystems (Linux, Windows oder OSX) vertraut machen, mit denen Sie mehrere Threads ausführen können. Sie sollten sich mit Parallelisierungs-APIs wie OpenMP und Threading Building Blocks oder OSX 10.6 "Snow Leopard", dem bevorstehenden "Grand Central", vertraut machen.

Sie sollten überlegen, ob Ihr Compiler automatisch parallelisiert werden soll oder ob der Autor der von Ihrem Compiler kompilierten Anwendungen spezielle Syntax- oder API-Aufrufe in sein Programm einfügen muss, um die Vorteile der mehreren Kerne zu nutzen.

9
Alex Brown

Der Assembly-Code wird in Maschinencode übersetzt, der auf einem Kern ausgeführt wird. Wenn Sie möchten, dass es mehrere Threads enthält, müssen Sie Betriebssystemprimitive verwenden, um diesen Code mehrmals auf verschiedenen Prozessoren oder verschiedene Codeteile auf verschiedenen Kernen zu starten. Jeder Kern führt einen separaten Thread aus. Jeder Thread sieht nur einen Kern, auf dem er gerade ausgeführt wird.

5
sharptooth

Es ist überhaupt nicht in Maschinenanweisungen gemacht; Die Kerne geben vor, unterschiedliche CPUs zu sein und haben keine besonderen Fähigkeiten, um miteinander zu sprechen. Sie kommunizieren auf zwei Arten:

  • sie teilen sich den physischen Adressraum. Die Hardware verwaltet die Cache-Kohärenz, sodass eine CPU in eine Speicheradresse schreibt, die eine andere liest.

  • sie teilen sich einen APIC (Programmable Interrupt Controller). Dies ist ein Speicher, der dem physischen Adressraum zugeordnet ist und von einem Prozessor verwendet werden kann, um die anderen zu steuern, sie ein- oder auszuschalten, Interrupts zu senden usw.

http://www.cheesecake.org/sac/smp.html ist eine gute Referenz mit einer dummen URL.

3
pjc50

Der Hauptunterschied zwischen einer Single- und einer Multi-Thread-Anwendung besteht darin, dass die erste einen Stapel und die zweite einen für jeden Thread hat. Code wird etwas anders generiert, da der Compiler davon ausgeht, dass die Daten- und Stapelsegmentregister (ds und ss) nicht gleich sind. Dies bedeutet, dass die Indirektion durch die ebp- und esp-Register, die standardmäßig das ss-Register verwenden, nicht auch standardmäßig ds verwendet (da ds! = Ss). Umgekehrt wird die Indirektion durch die anderen Register, die standardmäßig auf ds eingestellt sind, nicht standardmäßig auf ss eingestellt.

Die Threads teilen sich alles andere, einschließlich Daten- und Codebereiche. Sie teilen auch lib-Routinen, stellen also sicher, dass sie thread-sicher sind. Eine Prozedur, die einen Bereich in RAM sortiert, kann zur Beschleunigung mehrere Threads enthalten. Die Threads greifen dann auf Daten in demselben physischen Speicherbereich zu, vergleichen und ordnen sie und führen denselben Code aus, aber Verwendung verschiedener lokaler Variablen zur Steuerung ihres jeweiligen Teils der Sortierung. Dies liegt natürlich daran, dass die Threads unterschiedliche Stapel aufweisen, in denen die lokalen Variablen enthalten sind. Diese Art der Programmierung erfordert eine sorgfältige Anpassung des Codes, damit Datenkollisionen zwischen den Kernen (in Caches) auftreten und RAM) werden reduziert, was wiederum zu einem Code führt, der mit zwei oder mehr Threads schneller ist als mit nur einem. Natürlich ist ein nicht optimierter Code häufig mit einem Prozessor schneller als mit zwei oder mehr Dies ist eine größere Herausforderung, da der Standard-Haltepunkt "int 3" nicht anwendbar ist, da Sie einen bestimmten Thread und nicht alle unterbrechen möchten. Debug-Register-Haltepunkte lösen dieses Problem ebenfalls nicht, es sei denn, Sie können sie auf dem bestimmten Prozessor festlegen, der den SP ausführt bestimmten Thread, den Sie unterbrechen möchten.

Anderer Multithread-Code kann unterschiedliche Threads beinhalten, die in unterschiedlichen Teilen des Programms ausgeführt werden. Diese Art der Programmierung erfordert nicht die gleiche Einstellung und ist daher viel einfacher zu erlernen.

1
Olof Forshell

Bei jeder Architektur mit mehreren Prozessoren wurden im Vergleich zu den Einzelprozessor-Varianten Anweisungen für die Synchronisierung zwischen Kernen hinzugefügt. Außerdem verfügen Sie über Anweisungen zum Behandeln der Cache-Kohärenz, zum Leeren von Puffern und zu ähnlichen Operationen auf niedriger Ebene, mit denen ein Betriebssystem umgehen muss. Bei simultanen Multithreading-Architekturen wie IBM POWER6, IBM Cell, Sun Niagara und Intel "Hyperthreading" werden häufig neue Anweisungen zur Priorisierung zwischen Threads angezeigt (z. B. Festlegen von Prioritäten und explizites Ausgeben des Prozessors, wenn nichts zu tun ist). .

Die grundlegende Single-Thread-Semantik ist jedoch die gleiche. Sie fügen lediglich zusätzliche Funktionen hinzu, um die Synchronisation und Kommunikation mit anderen Kernen zu verwalten.

0
jakobengblom2