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

Когда-то, например, для написания ассемблера x86 вы должны были получить инструкции, в которых "загружать регистр EDX значением 5", "увеличивать регистр EDX" и т. Д.

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

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

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

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

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

11 ответов

Решение

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

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

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

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

mov edx, 0

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

Пример минимального запуска Intel x86 bare metal

Работоспособный пример из чистого металла со всеми необходимыми образцами. Все основные части описаны ниже.

Протестировано на Ubuntu 15.10 QEMU 2.3.0 и Lenovo ThinkPad T400.

Руководство Intel по системному программированию, том 3, 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

Ядро 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?
     */
    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! In gem5, CPU 1 starts
     * woken up from the start, So this is not needed.
     * TODO gem5 actually blows up if it tries to run this. Understad why.
     */
    /* PSCI 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
    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
    ldr x2, =semihost_args
    str x1, [x2, 0]
    mov x0, #0
    str x0, [x2, 8]
    mov x1, x2
    mov w0, 0x18
    hlt 0xf000
semihost_args:
    .skip 16

spinlock:
    .skip 8

GitHub вверх по течению.

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

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

Мне не удалось связаться с набором инструментов Ubuntu aarch64, но я предоставляю очень подробную рабочую настройку crosstool-NG:

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

Это какие-то особые привилегированные инструкции?

Нет. Все ядра работают в одной и той же памяти с одинаковыми старыми инструкциями.

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  1. Сделайте БПФ для некоторых данных
  2. Поместите результат в матрицу и найдите его собственные значения и собственные векторы.
  3. Отсортируйте последние по собственному значению
  4. повторить с первого шага с новыми данными

Что вы можете сделать, так это запустить шаг 2 по результатам шага 1 при выполнении шага 1 в другом ядре с новыми данными и запустить шаг 3 по результатам шага 2 в другом ядре, в то время как шаг 2 выполняется на следующих данных и шаге 1 работает с данными после этого. Вы можете сделать это в Compaq Visual Fortran и Intel Fortran, которые представляют собой эволюцию CVF, написав три отдельные программы/подпрограммы для трех шагов, и вместо одного «вызова» следующего он вызывает API для запуска своего потока. Они могут обмениваться данными, используя COMMON, который будет общей памятью данных для всех потоков. Вы должны изучать руководство, пока не заболит голова, и экспериментировать, пока не заработаете, но мне это удалось, по крайней мере, один раз.

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

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

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

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

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

Другие вопросы по тегам