Суперскалярность и внеочередное исполнение

Урок даёт обзор техник, которыми современные процессоры выжимают параллелизм из одного потока команд.

Суперскалярный процессор имеет несколько исполнительных устройств и запускает более одной команды за такт. Внеочередное исполнение (out-of-order) выполняет независимые команды раньше, не дожидаясь застрявших.

Зачем идти дальше конвейера

Конвейер доводит IPC до ~1 (одна команда за такт в идеале). Но почему бы не выполнять несколько команд за такт? Если у процессора два сумматора и два устройства памяти, независимые команды можно запускать одновременно. Это суперскалярность — главный источник производительности с 1990-х.

Внеочередное исполнение

Команды в программе идут по порядку, но не все зависят друг от друга. Если команда №2 ждёт данные из памяти, а команда №3 независима — зачем процессору простаивать? Внеочередной движок выполняет №3, пока №2 ждёт, а затем «собирает» результаты в правильном порядке. Промоделируем выигрыш на простом списке зависимостей:

# каждая команда: (имя, длительность, от_кого_зависит)
program = [
    ("A_load",  3, None),   # медленная загрузка из памяти
    ("B_add",   1, None),   # независима от A
    ("C_mul",   1, None),   # независима
    ("D_use_A", 1, "A_load")# зависит от A
]

def inorder(prog):
    t = 0
    for name, dur, dep in prog:
        t += dur                 # строго по очереди
    return t

def ooo(prog):
    # независимые идут параллельно; зависимая ждёт свою зависимость
    finish = {}
    parallel_time = max(d for _, d, dep in prog if dep is None)
    # A заканчивается на 3; D зависит от A -> 3 + 1
    a_done = next(d for n, d, dep in prog if n == "A_load")
    d_dur  = next(d for n, d, dep in prog if dep == "A_load")
    return max(parallel_time, a_done + d_dur)

print("по порядку (in-order):", inorder(program), "усл. тактов")
print("внеочередно (OoO):    ", ooo(program), "усл. тактов")

Вывод:

по порядку (in-order): 6 усл. тактов
внеочередно (OoO):     4 усл. тактов

Как работает под капотом

Внеочередной процессор — сложный механизм. Кратко о его частях:

БлокРоль
Переименование регистровубирает ложные зависимости по имени регистра
Окно команд / резервациихранит ждущие команды, запускает готовые
Несколько АЛУ/портовисполняют команды параллельно (суперскаляр)
Буфер переупорядочивания (ROB)возвращает результаты в исходном порядке программы

Спекулятивное исполнение идёт ещё дальше: процессор выполняет команды за предсказанным переходом до того, как узнал, верна ли догадка. Если нет — результаты отбрасываются. Это даёт скорость, но именно спекуляция породила знаменитые уязвимости Spectre/Meltdown.

Цена сложности

Внеочередной суперскалярный движок занимает огромную долю кристалла и энергии. Поэтому экономичные ядра (в телефонах, в «эффективных» ядрах гибридных CPU) часто делают проще — in-order или узко-суперскалярными — ради энергоэффективности.

Глубже в тему

Чтобы оценить, зачем нужна суперскалярность, полезно вспомнить предел простого конвейера: его теоретический потолок — IPC, равный единице, то есть одна завершённая команда за такт. Но в реальной программе нередко соседние команды независимы друг от друга, и нет физической причины исполнять их строго по очереди, если у процессора есть несколько исполнительных устройств. Суперскалярный процессор как раз и ставит рядом два-четыре-шесть АЛУ, отдельные блоки умножения, несколько портов доступа к памяти — и запускает на них независимые команды одновременно, поднимая IPC выше единицы. Это был главный источник роста производительности в 1990-х, когда наращивание частоты ещё не упёрлось в тепловую стену, но уже хотелось большего, чем давал одиночный конвейер.

Внеочередное исполнение решает проблему, которую суперскалярность сама по себе не закрывает: что делать, когда следующая по порядку команда застряла (например, ждёт данные из медленной памяти), а за ней стоят готовые к исполнению независимые команды? Процессор «по порядку» (in-order) был бы вынужден простаивать, пока застрявшая команда не разрешится, — даже имея свободные АЛУ. Внеочередной движок (out-of-order) заглядывает вперёд по потоку команд, находит готовые к исполнению и запускает их раньше, не дожидаясь застрявших. Расчёт из урока показывает суть: пока медленная загрузка тянется три такта, независимые сложение и умножение успевают исполниться «в обгон», и общее время падает с шести условных тактов до четырёх. Ключевая идея — извлекать параллелизм на уровне команд (ILP) из обычного последовательного кода, не требуя от программиста ничего.

Чтобы это работало корректно, недостаточно просто «запускать что готово»; нужны несколько хитрых механизмов, перечисленных в таблице урока, и стоит понять, зачем каждый. Переименование регистров устраняет ложные зависимости WAR и WAW: если две команды используют один и тот же архитектурный регистр, но без реальной передачи данных, им выдаются разные физические регистры, и они перестают мешать друг другу. Окно команд (резервационные станции) хранит ждущие команды и выпускает их, как только готовы операнды. А буфер переупорядочивания (ROB) решает важнейшую задачу: команды могут исполняться в любом порядке, но фиксировать результаты (commit) и менять видимое состояние программы они обязаны строго в исходном порядке. Это сохраняет иллюзию последовательного исполнения для программиста и, что критично, позволяет аккуратно откатить спекулятивные команды при неверном предсказании или прерывании.

За эту мощь приходится дорого платить, и понимание цены объясняет современный ландшафт процессоров. Внеочередной суперскалярный движок с глубоким окном команд, многопортовым регистровым файлом и сложным предсказателем занимает огромную долю кристалла и потребляет много энергии — большая часть транзисторов уходит не на собственно вычисления, а на «оркестровку» параллелизма. Поэтому в гибридных процессорах (например, big.LITTLE) производительные ядра делают широкими и внеочередными, а энергоэффективные — узкими и зачастую in-order, ради экономии батареи. Отдельная расплата — безопасность: спекулятивное исполнение, краеугольный камень этой архитектуры, оставляет в кэше микроскопические следы выполнения «не той» ветки, и атаки Spectre/Meltdown научились эти следы читать, вынудив индустрию вводить программные и аппаратные смягчения, стоящие части производительности. Это отрезвляющий итог раздела: десятилетия гонки за ILP принесли колоссальное ускорение, но обнажили глубокую связь между скоростью, энергией и безопасностью.

Частые ошибки

  • Путать суперскалярность и многоядерность. Суперскаляр — несколько команд за такт в одном ядре; многоядерность — несколько ядер.
  • Думать, что OoO нарушает порядок результатов. Команды исполняются вне порядка, но результаты фиксируются (commit) строго по порядку программы (ROB).
  • Считать спекуляцию безопасной. Она оставляет следы в кэше — основа атак Spectre/Meltdown.

Итог

  • Суперскаляр запускает несколько команд за такт через несколько исполнительных устройств.
  • Внеочередное исполнение выполняет независимые команды, пока другие ждут, повышая загрузку.
  • Спекуляция ускоряет, но создаёт уязвимости; всё это стоит площади и энергии.
Проверьте себя
1. Что означает «суперскалярный» процессор?
AУ него несколько ядер
BОн запускает более одной команды за такт благодаря нескольким исполнительным устройствам
CУ него высокая тактовая частота
DОн не использует конвейер
2. Зачем нужно внеочередное исполнение (out-of-order)?
AЧтобы перемешать результаты программы
BЧтобы выполнять независимые команды, пока другие ждут данные, не простаивая
CЧтобы уменьшить число регистров
DЧтобы отключить конвейер
3. Что общего у спекулятивного исполнения с уязвимостями Spectre/Meltdown?
AНичего
BСпекулятивно выполненные команды оставляют следы в кэше, которые можно прочитать
CОни отключают кэш
DОни замедляют процессор