Конвейер: как ускорить без роста частоты

Урок объясняет конвейеризацию — приём, позволяющий выполнять несколько команд одновременно на разных стадиях.

Конвейер (pipeline) — разбиение исполнения команды на ступени так, что разные команды одновременно находятся на разных ступенях, как изделия на сборочной линии.

Аналогия с прачечной

Представьте стирку: постирать (30 мин), высушить (30 мин), сложить (30 мин). Если делать четыре загрузки по очереди, целиком завершая каждую, уйдёт 4×90 = 360 минут. Но как только первая загрузка ушла в сушку, можно ставить вторую в стирку! Стиралка, сушилка и стол работают параллельно. Так четыре загрузки займут не 360, а 180 минут. Это и есть конвейер: ступени всегда заняты.

Пять классических ступеней RISC

IF  - Instruction Fetch   (выборка команды)
ID  - Instruction Decode  (декодирование + чтение регистров)
EX  - Execute             (АЛУ-операция)
MEM - Memory access       (доступ к памяти)
WB  - Write Back          (запись результата)

Без конвейера (по очереди):
 i1: IF ID EX MEM WB
 i2:                IF ID EX MEM WB
 i3:                               IF ID EX MEM WB

С конвейером (перекрытие):
 такт:  1  2  3  4  5  6  7
 i1:    IF ID EX MEM WB
 i2:       IF ID EX  MEM WB
 i3:          IF ID  EX  MEM WB
        каждый такт ЗАВЕРШАЕТСЯ одна команда!

Как работает под капотом: ускорение

В идеале конвейер из k ступеней даёт ускорение почти в k раз: после заполнения каждая ступень завершает по одной команде за такт. Но есть «разогрев» (заполнение конвейера) и «слив» в конце. Посчитаем точно:

def cycles_no_pipeline(n_instr, stages):
    return n_instr * stages

def cycles_pipeline(n_instr, stages):
    # первая команда проходит все ступени, далее +1 такт на команду
    return stages + (n_instr - 1)

for n in (1, 5, 100, 1000):
    seq = cycles_no_pipeline(n, 5)
    pipe = cycles_pipeline(n, 5)
    speedup = seq / pipe
    print(f"{n:>4} команд: без конв.={seq:>5} тактов | конв.={pipe:>5} | ускорение x{speedup:.2f}")

Вывод:

   1 команд: без конв.=    5 тактов | конв.=    5 | ускорение x1.00
   5 команд: без конв.=   25 тактов | конв.=    9 | ускорение x2.78
 100 команд: без конв.=  500 тактов | конв.=  104 | ускорение x4.81
1000 команд: без конв.= 5000 тактов | конв.= 1004 | ускорение x4.98

Видно: чем длиннее поток команд, тем ближе ускорение к числу ступеней (5). На большом потоке оно почти ×5 — конвейер «разогрелся» и работает на полную.

Пропускная способность против задержки

Конвейер не ускоряет одну команду (она по-прежнему проходит 5 ступеней). Он повышает пропускную способность (throughput) — сколько команд завершается в секунду. Это как конвейер на заводе: одна машина собирается столько же времени, но машин в час выходит куда больше.

Глубокие конвейеры

Можно дробить мельче: 10, 15, 20 ступеней. Тогда каждая ступень короче, значит такт можно сделать быстрее (выше частота). Но платой становятся бо́льшие потери при сбросе конвейера (см. конфликты переходов). Поэтому глубину выбирают как компромисс.

Глубже в тему

Почему конвейеризация оказалась таким революционным приёмом? Потому что она повышает производительность, почти не трогая физику. Можно было бы ускорять процессор, поднимая тактовую частоту, но частота упирается в тепло и пределы материалов. Конвейер же действует иначе: он не делает отдельную команду быстрее, а заставляет аппаратуру, которая раньше простаивала, всегда быть занятой. Пока АЛУ считает одну команду, блок выборки уже тащит следующую, а блок записи фиксирует результат позапрошлой. Это пример фундаментального инженерного принципа — повышать загрузку имеющихся ресурсов вместо наращивания их мощности. Аналогия с прачечной точна именно потому, что подчёркивает: ключ не в более быстрой стиралке, а в том, чтобы стиралка, сушилка и стол не простаивали.

Важно глубже понять разницу между задержкой (latency) и пропускной способностью (throughput), потому что их путаница порождает неверные ожидания. Задержка — это время от старта до финиша одной команды; она в конвейере не уменьшается, а иногда даже растёт (добавляются регистры-защёлки между ступенями, на которые тратится время). Пропускная способность — это сколько команд завершается в единицу времени; именно её конвейер кардинально повышает. Для пользователя важна обычно пропускная способность: программа состоит из миллионов команд, и нам всё равно, что каждая отдельная проходит пять ступеней, — важно, что в установившемся режиме каждый такт одна команда сходит с конвейера. Это та же логика, по которой завод меряют машинами в час, а не временем сборки одного автомобиля.

Идеальное ускорение, равное числу ступеней, — это потолок, к которому реальность лишь приближается. Мешают три фактора. Во-первых, разогрев и слив: пока конвейер заполняется первыми командами и опустошается последними, он работает не на полную, поэтому на коротких потоках выигрыш скромный (что хорошо видно в расчёте из урока: на 5 командах всего ×2,78). Во-вторых, ступени почти никогда не равны по длительности, а такт приходится подгонять под самую медленную ступень — остальные простаивают часть такта. В-третьих, конфликты (hazards), которым посвящён следующий урок, заставляют конвейер останавливаться. Реальное ускорение поэтому всегда меньше теоретического, и инженеры борются за каждую десятую долю.

Выбор глубины конвейера — это отдельная драматичная история архитектуры. Чем больше ступеней, тем короче каждая, а значит, можно поднять частоту. Pentium 4 от Intel пошёл по пути сверхглубокого конвейера (более 20, а в поздних версиях около 31 ступени), рассчитывая выиграть гонку гигагерц. Но платой стала чудовищная цена ошибки предсказания перехода: при сбросе терялись десятки уже начатых команд, и реальная производительность на ветвистом коде разочаровывала. Это привело к развороту индустрии: последующие архитектуры (Intel Core) вернулись к умеренной глубине (около 14 ступеней) и стали брать своё шириной (суперскалярностью) и умом (лучшими предсказателями), а не голой частотой. Урок истории: глубина конвейера — это компромисс, а не самоцель, и «больше ступеней» далеко не всегда означает «быстрее».

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

  • Думать, что конвейер ускоряет одну команду. Задержка одной команды та же; растёт пропускная способность потока.
  • Считать ускорение всегда равным числу ступеней. Мешают разогрев, конфликты и переходы; реальное ускорение меньше идеального.
  • Игнорировать цену глубины. Глубокий конвейер быстрее по частоте, но дороже при сбросах.

Итог

  • Конвейер перекрывает команды по ступеням (IF/ID/EX/MEM/WB) — как сборочная линия.
  • Ускорение стремится к числу ступеней на длинном потоке команд.
  • Растёт пропускная способность, а не скорость отдельной команды.
Проверьте себя
1. Что именно повышает конвейеризация?
AСкорость выполнения одной команды
BПропускную способность — число команд, завершаемых за единицу времени
CОбъём памяти
DТактовую частоту напрямую
2. К какому пределу стремится ускорение конвейера из 5 ступеней на длинном потоке команд?
A×1
B×2
C×5
D×10
3. В чём плата за более глубокий конвейер (больше ступеней)?
AМеньше регистров
BБо́льшие потери при сбросе конвейера (например, на неверно предсказанных переходах)
CНевозможность выполнять арифметику
DРост энергопотребления памяти