Как выглядит многоядерный язык ассемблера?

Когда-нибудь, чтобы написать ассемблер x86, например, у вас были бы инструкции, указывающие: "Загрузите регистр EDX со значением 5", "Increment EDX" и т.д.

С современными процессорами, имеющими 4 ядра (или даже больше), на уровне машинного кода это просто похоже на 4 отдельных процессора (т.е. есть только 4 разных "EDX" регистра)? Если это так, когда вы говорите "увеличивайте регистр EDX", что определяет, какой регистр ЦП EDX увеличивается? Есть ли в x86-ассемблере концепция "CPU context" или "thread"?

Как работает связь/синхронизация между ядрами?

Если вы писали операционную систему, какой механизм подвергается через аппаратное обеспечение, чтобы вы могли планировать выполнение на разных ядрах? Это какая-то специальная привилегированная инструкция (ы)?

Если вы пишете оптимизированную компилятор/байт-код VM для многоядерного процессора, что вам нужно знать конкретно о, скажем, x86, чтобы заставить его генерировать код, который эффективно работает во всех ядрах?

Какие изменения были внесены в машинный код x86 для поддержки многоядерных функций?

Ответ 1

Это не прямой ответ на вопрос, но это ответ на вопрос, который появляется в комментариях. По сути, вопрос заключается в том, какую поддержку предоставляет оборудование для многопоточной работы.

У Николаса Флинта было все в порядке, по крайней мере, относительно x86. В многопоточной среде (Hyper-threading, многоядерный или многопроцессорный) поток Bootstrap (обычно поток 0 в ядре 0 в процессоре 0) запускает выборку кода из адреса 0xfffffff0. Все остальные потоки запускаются в специальном состоянии ожидания Wait-for-SIPI. В рамках своей инициализации основной поток отправляет специальный межпроцессорный прерывание (IPI) поверх APIC, называемого SIPI (Startup IPI) для каждого потока, который находится в WFS. SIPI содержит адрес, из которого этот поток должен начинать получать код.

Этот механизм позволяет каждому потоку выполнять код с другого адреса. Все, что необходимо, - это поддержка программного обеспечения для каждого потока для создания собственных таблиц и очередей сообщений. ОС использует те, которые выполняют фактическое многопоточное планирование.

Что касается реальной сборки, как писал Николай, нет никакой разницы между сборками для однопоточного или многопоточного приложения. Каждый логический поток имеет свой собственный набор регистров, поэтому запись:

mov edx, 0

обновит только EDX для текущего потока. Нет способа изменить EDX на другой процессор, используя одну команду сборки. Вам нужен какой-то системный вызов, чтобы попросить ОС указать другой поток для запуска кода, который обновит его собственный EDX.

Ответ 2

Ссылки на родительские страницы ## Пример Intel x86 для минимального запуска без использования металла

Пример работоспособного голого металла со всем необходимым шаблоном. Все основные части описаны ниже.

Протестировано на Ubuntu 15.10 QEMU 2.3.0 и Lenovo ThinkPad T400 настоящий аппаратный гость.

Руководство по системному программированию Тома 3, Руководство по Intel - 325384-056RU сентябрь 2015 г. охватывает SMP в главах 8, 9 и 10.

Таблица 8-1. "Последовательность широковещательной передачи INIT-SIPI-SIPI и выбор тайм-аутов" содержит пример, который в основном работает:

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

В этом коде:

  1. Большинство операционных систем сделает невозможным большинство этих операций из кольца 3 (пользовательские программы).

    Поэтому вам нужно написать свое собственное ядро, чтобы свободно с ним играть: пользовательская программа Linux не будет работать.

  2. Сначала запускается один процессор, называемый процессором начальной загрузки (BSP).

    Он должен активировать другие (называемые процессорами приложений (AP)) через специальные прерывания, называемые Межпроцессорные прерывания (IPI).

    .Эти прерывания могут быть выполнены путем программирования расширенного программируемого контроллера прерываний (APIC) через регистр команд прерываний (ICR)

    Формат ICR задокументирован по адресу: 10.6 "ВЫПУСК МЕЖПРОЦЕССОРНЫХ ПРЕРЫВАНИЙ"

    IPI происходит, как только мы пишем в ICR.

  3. ICR_LOW определяется в 8.4.4 "Пример инициализации MP" как:

    ICR_LOW EQU 0FEE00300H
    

    Волшебное значение 0FEE00300 является адресом памяти ICR, как описано в Таблице 10-1 "Карта адресов локального регистра APIC"

  4. В примере используется самый простой из возможных методов: он устанавливает ICR для отправки широковещательных IPI, которые доставляются всем другим процессорам, кроме текущего.

    Но также возможно, и рекомендовано некоторыми, получить информацию о процессорах с помощью специальных структур данных, установленных BIOS, таких как таблицы ACPI или таблица конфигурации Intel MP, и только разбудить те, которые вам нужны по одному.

  5. XX в 000C46XXH кодирует адрес первой инструкции, которую процессор будет выполнять как:

    CS = XX * 0x100
    IP = 0
    

    Помните, что CS умножает адреса на 0x10, поэтому фактический адрес памяти для первой инструкции:

    XX * 0x1000
    

    Так что если, например, XX == 1, процессор будет запускаться с 0x1000.

    Затем мы должны убедиться, что в этом месте памяти должен быть запущен 16-битный код реального режима, например с:

    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
    

    Использование сценария компоновщика - еще одна возможность.

  6. Циклы задержки - раздражающая часть, чтобы начать работать: не существует супер простого способа точно сделать такие сны.

    Возможные методы включают в себя:

    • PIT (используется в моем примере)
    • HPET
    • откалибруйте время занятого цикла с помощью вышеизложенного и используйте его вместо этого

    По теме: Как вывести число на экран и так и спать одну секунду со сборкой DOS x86?

  7. Я думаю, что исходный процессор должен быть в защищенном режиме, чтобы он работал, когда мы пишем по адресу 0FEE00300H, который слишком велик для 16-битных данных

  8. Для связи между процессорами мы можем использовать спин-блокировку основного процесса и изменить блокировку из второго ядра.

    Мы должны убедиться, что обратная запись в память выполнена, например, до wbinvd.

Общее состояние между процессорами

8.7.1 "Состояние логических процессоров" гласит:

Следующие функции являются частью архитектурного состояния логических процессоров в процессорах Intel 64 или IA-32 поддержка технологии Intel Hyper-Threading. Функции можно разделить на три группы:

  • Дублируется для каждого логического процессора
  • Разделяется логическими процессорами в физическом процессоре
  • Разделяется или дублируется, в зависимости от реализации

Следующие функции дублируются для каждого логического процессора:

  • Регистры общего назначения (EAX, EBX, ECX, EDX, ESI, EDI, ESP и EBP)
  • Сегментные регистры (CS, DS, SS, ES, FS и GS)
  • EFLAGS и EIP регистры. Обратите внимание, что регистры CS и EIP/RIP для каждого логического процессора указывают на поток инструкций для потока, выполняемого логическим процессором.
  • Регистры FPU x87 (ST0-ST7, слово состояния, слово управления, слово тега, указатель операнда данных и инструкция указатель)
  • MMX регистры (от MM0 до MM7)
  • Регистры XMM (от XMM0 до XMM7) и регистр MXCSR
  • Регистры управления и регистры указателей системной таблицы (GDTR, LDTR, IDTR, регистр задач)
  • Регистры отладки (DR0, DR1, DR2, DR3, DR6, DR7) и MSR управления отладкой
  • Проверка состояния компьютера (IA32_MCG_STATUS) и возможности проверки компьютера (IA32_MCG_CAP) MSR
  • Модуляция тепловых часов и управление питанием ACPI MSR
  • Счетчик меток времени MSR
  • Большинство других регистров MSR, включая таблицу атрибутов страницы (PAT). Смотрите исключения ниже.
  • Местные регистры APIC.
  • Дополнительные регистры общего назначения (R8-R15), регистры XMM (XMM8-XMM15), управляющий регистр, IA32_EFER вкл. Процессоры Intel 64.

Следующие функции совместно используются логическими процессорами:

  • Регистры диапазонов типов памяти (MTRR)

Совместное использование или дублирование следующих функций зависит от реализации:

  • IA32_MISC_ENABLE MSR (адрес MSR 1A0H)
  • MSR архитектуры машинной проверки (MCA) (за исключением MSR IA32_MCG_STATUS и IA32_MCG_CAP)
  • Контроль производительности и счетчик MSR

Совместное использование кэша обсуждается по адресу:

Гиперпотоки Intel имеют больший общий доступ к кешу и конвейеру, чем отдельные ядра: https://superuser.com/questions/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858

Ядро Linux 4.2

Основное действие инициализации, кажется, находится в arch/x86/kernel/smpboot.c.

ARM минимальный работоспособный пример из чистого металла

Здесь я приведу минимальный исполняемый пример ARMv8 aarch64 для 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.

Собрать и запустить:

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

В этом примере мы помещаем CPU 0 в цикл спин-блокировки, и он завершается только с CPU 1, освобождающим спин-блокировку.

После спин-блокировки ЦП 0 затем выполняет вызов выхода из полухоста, который заставляет QEMU завершиться.

Если вы запускаете QEMU только с одним процессором с -smp 1, то симуляция просто навсегда зависает на спин-блокировке.

CPU 1 разбудился с помощью интерфейса PSCI, более подробную информацию можно найти по адресу: ARM: запуск/пробуждение/включение других ядер /AP CPU и передача начального адреса выполнения?

вышестоящая версия также имеет несколько настроек, чтобы она работала на gem5, так что вы также можете поэкспериментировать с характеристиками производительности.

Я не тестировал его на реальном оборудовании, поэтому не уверен, насколько это портативно. Следующая библиография Raspberry Pi может представлять интерес:

Этот документ содержит некоторые рекомендации по использованию примитивов синхронизации ARM, которые затем можно использовать для забавных вещей с несколькими ядрами: http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitives.pdf

Протестировано на Ubuntu 18.10, GCC 8.2.0, Binutils 2.31.1, QEMU 2.12.0.

Следующие шаги для более удобного программирования

Предыдущие примеры пробуждают вторичный процессор и выполняют базовую синхронизацию памяти с выделенными инструкциями, что является хорошим началом.

Но чтобы сделать многоядерные системы простыми в программировании, например, как и POSIX pthreads, вам также необходимо перейти к следующим более сложным темам:

  • Программа установки прерывает и запускает таймер, который периодически решает, какой поток будет запущен сейчас. Это известно как упреждающая многопоточность.

    Такая система также должна сохранять и восстанавливать регистры потоков по мере их запуска и остановки.

    Также возможно иметь не вытесняющие многозадачные системы, но они могут потребовать, чтобы вы изменили свой код так, чтобы каждый поток давал результат (например, с реализацией pthread_yield), и стало бы труднее балансировать рабочие нагрузки.

    Вот несколько упрощенных примеров таймера с "голым металлом":

  • иметь дело с конфликтами памяти. В частности, каждому потоку потребуется уникальный стек, если вы хотите кодировать на C или других языках высокого уровня.

    Можно просто ограничить потоки фиксированным максимальным размером стека, но лучший способ справиться с этим - использовать пейджинг, который позволяет создавать эффективные стеки "неограниченного размера".

    Вот простой пример aarch64 baremetal, который взорвется, если стек станет слишком глубоким

Вот несколько веских причин использовать ядро Linux или другую операционную систему :-)

Примитивы синхронизации пользовательской памяти

Хотя запуск/остановка/управление потоком, как правило, выходят за рамки пользовательской области, вы можете использовать инструкции по сборке из потоков пользовательской области для синхронизации обращений к памяти без потенциально более дорогих системных вызовов.

Конечно, вы должны предпочесть использовать библиотеки, которые переносят эти низкоуровневые примитивы. Сам стандарт C++ значительно продвинулся в заголовке <atomic>, в частности в std::memory_order. Я не уверен, охватывает ли он всю возможную семантику памяти, но это возможно.

Более тонкая семантика особенно актуальна в контексте структур данных без блокировки, которые в некоторых случаях могут повысить производительность. Чтобы реализовать их, вам, вероятно, придется немного узнать о различных типах барьеров памяти: https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/

Boost, например, имеет несколько реализаций контейнера без блокировки: https://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html

Такие пользовательские инструкции, похоже, также используются для реализации системного вызова Linux futex, который является одним из основных примитивов синхронизации в Linux. man futex 4.15 гласит:

Системный вызов futex() предоставляет метод для ожидания, пока определенное условие не станет истинным. Обычно используется в качестве блокирующая конструкция в контексте синхронизации с разделяемой памятью. При использовании фьютексов большая часть синхронизации Операции выполняются в пользовательском пространстве. программа пользовательского пространства использует системный вызов futex() только тогда, когда есть вероятность, что Программа должна блокироваться на более длительное время, пока условие не станет истинным. Другие операции futex() могут использоваться для пробуждения любого процессы или потоки, ожидающие определенного условия.

Само имя системного вызова означает "Fast Userspace XXX".

Вот минимальный бесполезный пример C++ x86_64/aarch64 со встроенной сборкой, который иллюстрирует основное использование таких инструкций в основном для развлечения:

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.

Возможный вывод:

my_non_atomic_ulong 15264
my_arch_non_atomic_ulong 15267

Из этого мы видим, что инструкция x86 LOCK prefix/aarch64 LDADD сделала добавление атомарным: без него у нас есть условия гонки для многих добавлений, и общее количество в конце меньше, чем синхронизированные 20000.

См. также: Что означает " блокировка " значит инструкция в сборке х86?

Протестировано в Ubuntu 19.04 amd64 и в пользовательском режиме QEMU aarch64.

Ответ 3

Как я понимаю, каждое "ядро" представляет собой полный процессор с собственным набором регистров. В принципе, BIOS запускает вас с одного ядра, а затем операционная система может "запускать" другие ядра, инициализируя их и указывая на код для запуска и т.д.

Синхронизация выполняется ОС. Как правило, каждый процессор запускает другой процесс для ОС, поэтому функции многопоточности операционной системы отвечают за решение, какой процесс будет касаться какой памяти и что делать в случае столкновения с памятью.

Ответ 4

Неофициальный SMP FAQ логотип


Когда-то, например, для написания ассемблера x86, у вас были бы инструкции, указывающие "загрузить регистр EDX со значением 5", "увеличить регистр EDX" и т.д. С современными процессорами, имеющими 4 ядра (или даже больше), на уровне машинного кода это просто похоже на 4 отдельных процессора (т.е. есть только 4 разных "EDX" регистра)?

Совершенно верно. Существует 4 набора регистров, включая 4 отдельных указателя инструкций.

Если это так, когда вы говорите "увеличивайте регистр EDX", что определяет, какой регистр ЦП EDX увеличивается?

Процессор, который выполнил эту инструкцию, естественно. Подумайте об этом как о 4 совершенно разных микропроцессорах, которые просто используют одну и ту же память.

Есть ли теперь концепция "CPU context" или "thread" в ассемблере x86?

Нет. Ассемблер просто переводит инструкции, как это всегда делалось. Там нет изменений.

Как работает связь/синхронизация между ядрами?

Поскольку они имеют одну и ту же память, это в основном вопрос программной логики. Хотя теперь существует механизм межпроцессорного прерывания, он не нужен и первоначально не присутствовал в первых двухпроцессорных системах x86.

Если вы пишете операционную систему, какой механизм подвергается через аппаратное обеспечение, чтобы вы могли планировать выполнение на разных ядрах?

Планировщик фактически не изменяется, за исключением того, что он немного более осторожен относительно критических разделов и типов используемых замков. Перед SMP код ядра в конечном итоге вызовет планировщик, который будет смотреть на очередь выполнения и выбрать процесс для запуска в качестве следующего потока. (Процессы для ядра очень похожи на потоки.) Ядро SMP запускает один и тот же код, по одному потоку за раз, именно так, теперь критическая блокировка раздела должна быть SMP-безопасной, чтобы убедиться, что два ядра не могут случайно выбрать тот же PID.

Это какая-то специальная привилегированная инструкция (ы)?

Нет. Ядра всего лишь работают в одной и той же памяти с теми же старыми инструкциями.

Если вы пишете оптимизированную компилятор/байт-код VM для многоядерного процессора, что вам нужно знать конкретно о, скажем, x86, чтобы заставить его генерировать код, который эффективно работает во всех ядрах?

Вы запускаете тот же код, что и раньше. Это ядро ​​Unix или Windows, которое необходимо изменить.

Вы можете подвести мой вопрос как "Какие изменения были внесены в машинный код x86 для поддержки многоядерных функций?"

Ничего не было необходимо. Первые системы SMP использовали тот же набор команд, что и однопроцессорные. Теперь появилась значительная эволюция архитектуры x86 и создание новых инструкций для ускорения работы, но для SMP не было необходимости.

Для получения дополнительной информации см. Спецификация Intel для многопроцессорных технологий.


Обновление: на все последующие вопросы можно ответить, просто полностью признав, что n-way многоядерный процессор почти 1 точно такой же, как n отдельных процессоров, которые просто используют одну и ту же память. 2 Был задан важный вопрос: как программа написана для работы на нескольких ядрах для большей производительности? И ответ: он написан с использованием библиотеки потоков, например Pthreads. В некоторых библиотеках потоков используются "зеленые потоки", которые не видны ОС, и те не получат отдельные ядра, но пока библиотека потоков использует функции потока ядра, ваша потоковая программа будет автоматически многоядерной.
1. Для обратной совместимости только первый ядро ​​запускается в reset, и для запуска остальных нужно сделать несколько вещей типа драйвера.
2. Естественно, они также используют все периферийные устройства.

Ответ 5

Если вы пишете оптимизацию компилятор/байт-код VM для многоядерного CPU, что вам нужно знать конкретно о, скажем, x86, чтобы сделать он генерирует эффективный код по всем ядрам?

Как кто-то, кто пишет оптимизацию виртуальных машин компилятора/байт-кода, я могу помочь вам здесь.

Вам не нужно знать ничего конкретно о x86, чтобы заставить его генерировать код, который эффективно работает во всех ядрах.

Однако вам может понадобиться узнать о cmpxchg и друзьях, чтобы написать код, который работает правильно во всех ядрах. Для многоядерного программирования требуется синхронизация и связь между потоками исполнения.

Возможно, вам нужно что-то узнать о x86, чтобы он генерировал код, который эффективно работает на x86 в целом.

Есть другие вещи, которые вам было бы полезно изучить:

Вы должны узнать об оборудовании, предоставляемом ОС (Linux или Windows или OSX), чтобы вы могли запускать несколько потоков. Вы должны узнать о API-интерфейсах параллелизации, таких как OpenMP и Threading Building Blocks, или OSX 10.6 "Snow Leopard", предстоящий "Grand Central".

Вы должны подумать, должен ли ваш компилятор быть автоматически параллелизирован, или если автор приложений, скомпилированный вашим компилятором, должен добавить в свою программу специальные синтаксические или API-вызовы, чтобы воспользоваться преимуществами нескольких ядер.

Ответ 6

Каждое ядро ​​выполняется из другой области памяти. Ваша операционная система укажет ядро ​​на вашу программу, и ядро ​​выполнит вашу программу. Ваша программа не будет знать, что существует более одного ядра или на каком ядре оно выполняется.

Также нет дополнительной инструкции, доступной только для операционной системы. Эти сердечники идентичны одноядерным чипам. Каждое ядро ​​выполняет часть операционной системы, которая будет обрабатывать связь с областями общей памяти, используемыми для обмена информацией, чтобы найти следующую область памяти для выполнения.

Это упрощение, но оно дает вам основную идею о том, как это делается. Подробнее о многоядерных и многопроцессорных системах на Embedded.com есть много информации об этой теме... Этот раздел очень быстро усложняется!

Ответ 7

Код сборки преобразуется в машинный код, который будет выполняться на одном ядре. Если вы хотите, чтобы он был многопоточным, вам придется использовать примитивы операционной системы, чтобы запустить этот код на разных процессорах несколько раз или разные фрагменты кода на разных ядрах - каждое ядро ​​будет выполнять отдельный поток. Каждый поток будет видеть только одно ядро, в котором он выполняется.

Ответ 8

Это вообще не выполняется в машинных инструкциях; ядра притворяются отличными процессорами и не имеют особых возможностей для общения друг с другом. Существует два способа общения:

  • они разделяют физическое адресное пространство. Аппаратное обеспечение когерентности кэширования, поэтому один процессор записывает на адрес памяти, который другой читает.

  • они совместно используют APIC (программируемый контроллер прерываний). Это память, отображаемая в физическом адресном пространстве, и может использоваться одним процессором для управления другими, включать или выключать их, отправлять прерывания и т.д.

http://www.cheesecake.org/sac/smp.html - хорошая ссылка с глупым адресом.

Ответ 9

Основное различие между одно- и многопоточным приложением состоит в том, что первый имеет один стек, а последний имеет по одному для каждого потока. Код генерируется несколько иначе, поскольку компилятор предполагает, что регистры сегментов данных и стека (ds и ss) не равны. Это означает, что косвенность через регистры ebp и esp, которые по умолчанию соответствуют регистру ss, также не будет по умолчанию для ds (потому что ds!= Ss). И наоборот, косвенное обращение через другие регистры, для которых по умолчанию ds не будет использоваться по умолчанию ss.

В потоках есть все остальное, включая области данных и кода. Они также совместно используют подпрограммы lib, поэтому убедитесь, что они потокобезопасны. Процедура, которая сортирует область в ОЗУ, может быть многопоточной, чтобы ускорить работу. Затем потоки будут получать доступ, сравнивать и упорядочивать данные в одной и той же физической области памяти и выполнять один и тот же код, но с использованием разных локальных переменных для управления своей соответствующей частью сортировки. Это, конечно, связано с тем, что потоки имеют разные стеки, в которых содержатся локальные переменные. Для этого типа программирования требуется тщательная настройка кода, чтобы уменьшить межъядерные коллизии данных (в кэшах и ОЗУ), что, в свою очередь, приводит к коду, который быстрее с двумя или более потоками, чем с одним. Конечно, некорректный код часто будет быстрее с одним процессором, чем с двумя или более. Отладка более сложная, поскольку стандартная точка останова "int 3" не будет применяться, так как вы хотите прервать определенный поток, а не все из них. Контрольные точки реестра Debug не решают эту проблему либо, если вы не можете установить их на конкретном процессоре, выполняющем определенный поток, который вы хотите прервать.

Другой многопоточный код может включать в себя разные потоки, выполняемые в разных частях программы. Этот тип программирования не требует такой же настройки, и поэтому его намного легче изучить.

Ответ 10

Что было добавлено в каждой архитектуре, совместимой с несколькими процессами, по сравнению с однопроцессорными вариантами, которые были перед ними, являются инструкциями по синхронизации между ядрами. Кроме того, у вас есть инструкции для работы с когерентностью кэша, буферами очистки и аналогичными низкоуровневыми операциями, с которыми приходится иметь ОС. В случае одновременных многопоточных архитектур, таких как IBM POWER6, IBM Cell, Sun Niagara и Intel "Hyperthreading", вы также склонны видеть новые инструкции для определения приоритетов между потоками (например, устанавливать приоритеты и явно давать процессор, когда нечего делать),

Но основная семантика одного потока одинакова, вы просто добавляете дополнительные возможности для обработки синхронизации и связи с другими ядрами.