Конфликты конвейера: hazards
Урок разбирает конфликты (hazards) — ситуации, ломающие гладкую работу конвейера.
Конфликт конвейера (hazard) — ситуация, когда следующая команда не может выполниться в свой такт из-за зависимости от ресурсов, данных или ещё не вычисленного перехода.
Зачем знать про конфликты
Идеальное ускорение ×k достижимо только в теории. На практике команды зависят друг от друга, и конвейер вынужден «спотыкаться». Понимание трёх типов конфликтов объясняет, почему реальные процессоры сложнее наивной сборочной линии и зачем им форвардинг, остановки и предсказание переходов.
Три типа конфликтов
| Тип | Причина | Пример |
| Структурный | две команды борются за один ресурс | обе хотят память в один такт |
| Данных (data) | команда ждёт результат предыдущей | R1 ещё не записан, а уже нужен |
| Управления (control) | переход меняет ход, но ещё не вычислен | что выбирать после ветвления? |
Конфликт данных подробно
Самый частый. Рассмотрим:
ADD R1, R2, R3 ; R1 вычисляется
SUB R4, R1, R5 ; R4 зависит от R1 -- но R1 ещё НЕ записан!
Вторая команда хочет читать R1 на ступени ID, но первая запишет R1 только на ступени WB — на 3 такта позже. Без мер вторая команда прочитает старое значение R1. Смоделируем это «наивным» конвейером и увидим ошибку:
def naive_pipeline():
# такты, на которых команда читает (ID) и пишет (WB) регистры
# i1: ADD R1 = R2+R3 ; пишет R1 на такте 5 (WB)
# i2: SUB R4 = R1-R5 ; читает R1 на такте 3 (ID)
write_R1_at = 5
read_R1_at = 3
if read_R1_at < write_R1_at:
return "КОНФЛИКТ: R1 читается на такте 3, а пишется на такте 5"
return "ок"
print(naive_pipeline())
print("Решение: остановка (stall) или форвардинг (следующий урок).")Вывод:
КОНФЛИКТ: R1 читается на такте 3, а пишется на такте 5 Решение: остановка (stall) или форвардинг (следующий урок).
Конфликт управления (переходы)
Когда конвейер встречает условный переход, он ещё не знает, куда пойдёт исполнение, — а команды надо выбирать каждый такт. Если процессор «угадал» неверно, уже начатые команды (за ветвлением) приходится отбрасывать. Посчитаем цену неверного угадывания:
def branch_penalty(stages_before_resolve, n_branches, miss_rate):
# каждый неверно предсказанный переход стоит несколько отброшенных команд
wasted = n_branches * miss_rate * stages_before_resolve
return wasted
# 1000 переходов, переход решается на 3-й ступени, 20% промахов
lost = branch_penalty(stages_before_resolve=3, n_branches=1000, miss_rate=0.2)
print(f"Потеряно тактов из-за неверных переходов: {lost:.0f}")Вывод:
Потеряно тактов из-за неверных переходов: 600
Как работает под капотом: структурный конфликт
Если у процессора одна память для команд и данных (фон Нейман!), то в один такт команда на ступени IF хочет читать команду, а другая на ступени MEM — данные. Конфликт за память. Решение — раздельные кэши L1i/L1d (гарвардский приём), и структурный конфликт исчезает.
Глубже в тему
Конфликты конвейера — это цена, которую приходится платить за иллюзию параллельного исполнения последовательной по своей природе программы. Программист пишет команды так, будто каждая полностью завершается перед началом следующей; конвейер же нарушает это допущение, держа несколько команд «в полёте» одновременно. Пока команды независимы, обман работает безупречно. Но реальный код пронизан зависимостями: результат одной операции тут же используется в следующей, переходы определяют, какие команды вообще исполнятся. Конфликты — это места, где обман вскрывается и аппаратуре приходится принимать меры, чтобы программа осталась корректной. Понимание трёх типов конфликтов объясняет, почему реальный IPC (число команд за такт) почти всегда ниже теоретического идеала.
Стоит точнее разобрать терминологию конфликтов данных, потому что в литературе их делят на подвиды. Показанный в уроке случай — это RAW (Read After Write, «чтение после записи»): команда читает регистр, который предыдущая ещё не записала. Это истинная зависимость по данным, отражающая реальный поток вычислений, и устранить её алгоритмически нельзя. Но бывают и «ложные» зависимости — WAR (запись после чтения) и WAW (запись после записи), которые возникают не из-за реального потока данных, а из-за повторного использования имени регистра. Их можно убрать переименованием регистров (этим занимаются внеочередные процессоры из раздела 8). Различие принципиально: истинные зависимости — фундаментальный предел параллелизма, ложные — артефакт ограниченного числа регистров, который аппаратура умеет обходить.
Конфликт управления заслуживает отдельного осмысления, потому что он, как правило, дороже конфликтов данных. Когда конвейер встречает условный переход, он физически обязан выбирать какую-то команду на следующем такте — простаивать он не может. Но исход перехода ещё не вычислен, поэтому процессор делает ставку (предсказание) и спекулятивно гонит команды выбранной ветки. Если ставка не сыграла, все спекулятивно начатые команды нужно отбросить (flush), а это тем больнее, чем глубже конвейер: к моменту разрешения перехода в конвейер могло войти много «неправильных» команд. Расчёт из урока показывает масштаб: тысяча переходов с 20% промахов и тремя потерянными тактами на каждый промах съедает 600 тактов впустую. На ветвистом коде (интерпретаторы, парсеры) это становится доминирующей статьёй потерь.
Структурные конфликты в современных процессорах встречаются реже двух других именно потому, что архитекторы сознательно дублируют ресурсы, чтобы их избежать. Классический пример — единая память для команд и данных в чистой фон-неймановской машине: на одном такте ступень IF хочет читать команду, а ступень MEM — данные, и они дерутся за единственный порт памяти. Решение, заимствованное у гарвардской архитектуры, — раздельные кэши первого уровня L1i (для команд) и L1d (для данных); теперь оба обращения идут параллельно, и конфликт исчезает. Аналогично дублируют порты регистрового файла (чтобы две команды могли читать регистры одновременно) и добавляют несколько АЛУ. Это иллюстрирует общую философию: многие конфликты дешевле предотвратить дублированием железа, чем разрешать остановками. Но дублирование стоит площади кристалла и энергии, поэтому экономичные ядра идут на компромиссы, принимая часть структурных конфликтов ради простоты.
Частые ошибки
- Считать конфликты редкостью. В реальном коде зависимости данных встречаются постоянно; именно из-за них реальный IPC далёк от идеала.
- Путать типы конфликтов. Структурный — про ресурсы, данных — про значения, управления — про переходы.
- Думать, что конфликт = ошибка. Процессор не ошибается: он разрешает конфликты остановками и форвардингом, но теряет такты.
Итог
- Конфликты мешают идеальному ускорению конвейера; их три типа.
- Структурный — борьба за ресурс; данных — зависимость от результата; управления — неизвестный исход перехода.
- Конфликты данных и переходов — главная причина, по которой реальный IPC ниже теоретического.