Логика, а не магия: как работает предсказатель переходов
Если бы вы могли заглянуть внутрь современного процессора во время его работы, вы бы не увидели ничего, что напоминает традиционные вычисления. Там нет цифр, которые складываются в столбик, и нет программного кода, который выполняется построчно, как в книге. Вместо этого вы бы увидели нечто, больше похожее на гигантский, безупречно отлаженный конвейерный завод.
На этом «заводе» обработка каждой команды разбита на десятки мелких этапов. Пока одна команда только расшифровывается, следующая уже берётся из памяти, а предыдущая — записывает результат. Этот принцип так и называется конвейером (pipeline). Он позволяет процессору работать гораздо эффективнее, выполняя множество задач одновременно.
Но у этого конвейера есть одна огромная проблема. Представьте, что на ленту постоянно поступают коробки, и ваша задача отправлять их в одном из двух направлений. Вы не можете просто посмотреть на коробку, чтобы принять верное решение. Вам нужно её вскрыть, проверить содержимое, и только тогда вы поймёте, куда её направить. А конвейер тем временем продолжает двигаться, и следующие коробки уже начинают давить сзади. Чтобы не случился затор, вам приходится угадывать, куда отправить коробку, ещё до того, как вы её открыли. Если вы угадали правильно — конвейер работает без остановки. Если ошиблись — всё приходится останавливать и перекладывать заново, теряя драгоценное время.
Именно такую задачу ежесекундно решает процессор, сталкиваясь с командами «условного перехода» — это те самые if и else, которые есть в любом языке программирования. Они заставляют процессор делать выбор. И чтобы конвейер не простаивал, в процессор встроен специальный механизм, называемый предсказателем переходов. Это сложный узел, который не вычисляет, а именно угадывает результат решения, позволяя процессору продолжать работу. В этой статье мы разберем, как эта технология появилась, как она устроена изнутри, и почему без неё любой современный компьютер был бы гораздо медленнее.
Проблема — почему условные переходы тормозят процессор
Принцип конвейера
Давайте разберём принцип конвейера подробнее, потому что без него невозможно понять всю остроту проблемы. Выполнение одной команды процессором — это не мгновенный акт. Оно состоит из последовательных этапов. Упрощенно, этих этапов пять:
Представьте мойку машин. Один человек намыливает, второй смывает пену, третий вытирает насухо. Пока первый намыливает вторую машину, второй смывает пену с первой, а третий вытирает нулевую (как правило, счёт в компьютерных системах идёт с нуля). Производительность такой линии в разы выше, чем если бы машины мылись по одной.
Так же и в процессоре. В идеальной ситуации на каждом из пяти этапов одновременно находится своя команда. За один такт работы процессора каждая команда переходит на следующий этап. Это означает, что хотя выполнение одной команды по-прежнему занимает 5 тактов, готовый результат процессор начинает выдавать на каждом такте, а не раз в пять. Производительность повышается.
Условные переходы и «пузырь» в конвейере
Теперь посмотрим, что происходит, когда процессор встречает команду условного перехода. Это команда, которая говорит: «Сначала проверь условие, и в зависимости от результата, выполняй следующую команду либо из одного места в памяти, либо из другого».
Вот простой пример на псевдокоде, понятном даже тем, кто не программирует:
сравнить значение (X) с числом 10;
если X больше 10, перейти к команде A;
иначе перейти к команде B.
Команда A могла бы быть print("Большое число"), а команда B — print("Маленькое число").
Проблема для конвейера заключается в следующем: на этапах IF и ID процессор ещё не знает результата сравнения X> 10. Результат будет готов только после этапа EX (выполнения) команды сравнения. А тем временем конвейеру нужно немедленно загружать следующую команду. Какую ему выбрать? Команду A или команду B?
Процессор оказывается перед выбором вслепую. И здесь возможны два сценария:
Что происходит при ошибке предсказания?
Допустим, процессор предположил, что X будет меньше 10, и начал загружать команды из ветки B. Он уже потратил несколько тактов, продвинув эти команды по этапам конвейера. Но вот вычисление завершилось, и оказалось, что X равен 15. Это значит, что нужно было выполнять команды из ветки A. Всё, что процессор успел сделать для ветки B — бесполезно. Теперь ему нужно:
Именно эта цена ошибки и заставила инженеров задуматься: как угадывать результат перехода как можно точнее? Так родилась и начала эволюционировать одна из самых важных технологий для повышения производительности — предсказатель переходов.
История — эволюция угадывания
Предсказатель переходов не родился сложным. Его эволюция — это путь от самых примитивных догадок до высокоинтеллектуальных систем, которые можно назвать одним из видов аппаратного искусственного интеллекта. Инженеры на каждом этапе решали конкретные проблемы своих предшественников, делая механизм всё умнее.
Статическое предсказание (ранние процессоры)
В первых конвейерных процессорах, столкнувшихся с проблемой условных переходов, одним из решений было статическое предсказание. Инженеры закладывали в чип одно неизменное правило, которое применялось ко всем переходам без разбора.
Было два основных варианта такого правила:
Какой вариант был лучше? Это зависело от структуры программ. Например, циклы for или while большую часть времени заставляют процессор переходить назад, чтобы повторить код. Для них правило «переход всегда выполняется» оказывалось точнее. А для проверки ошибок, которые возникают редко, лучше работало правило «переход всегда не выполняется».
Однако в среднем точность статического предсказания редко превышала 60%. Это ненамного лучше, чем подбрасывать монетку. Каждое второе предсказание приводило к дорогостоящему сбросу конвейера. Стало ясно, что универсальное правило не работает. Нужен был механизм, который мог бы «запоминать» поведение каждого конкретного перехода.
Динамическое предсказание (прорыв)
Идея динамического предсказания стала ключевым прорывом. Её суть в следующем: процессор хранит историю недавних результатов для каждого отдельного условного перехода (точнее, для его адреса в памяти) и на основе этой истории принимает решение.
Первой практической реализацией этой идеи стали однобитные предсказатели.
Это уже был огромный шаг вперёд. Процессор научился распознавать простые паттерны (шаблоны). Например, если какая-то проверка в программе почти всегда ложная (например, проверка на редкую ошибку), предсказатель после первого же прогона запомнит это и будет стабильно предсказывать «не выполняется», почти не ошибаясь.
Но у этой схемы обнаружился серьёзный недостаток. Рассмотрим цикл, который повторяется 100 раз. Внутри него есть условие. На 99 итерациях цикла переход выполняется, и на последней, сотой — не выполняется, чтобы выйти из цикла.
Проблема в том, что однобитная схема лишена «памяти» о долгосрочных трендах. Она слишком остро реагирует на любую единственную ошибку. Даже если это была редкая аномалия в устойчивом паттерне.
Решение этой проблемы пришло с внедрением двухбитных счётчиков. Это был качественный скачок в точности.
Такой счётчик — это уже не просто флаг, а автомат с четырьмя состояниями. Условно их можно обозначить так:
Правила изменения состояний просты:
Двухбитные предсказатели обеспечили точность около 90-95% для типичных программ и надолго стали основой технологии.
Современные сложные системы
Программы становились сложнее, и инженеры поняли, что поведения перехода, основанного только на его собственной истории, недостаточно. Иногда результат одного перехода зависит от результатов нескольких предыдущих, совершенно других переходов. Это, как если бы решение «идти ли сегодня гулять» зависело не только от того, шёл ли дождь вчера, но и от того, были ли у вас другие планы и какой был день недели.
Для улавливания таких сложных глобальных паттернов были разработаны новые схемы:
Предсказание с глобальной историей
Процессор хранит не отдельную историю для каждого перехода, а единый битовый регистр, который фиксирует результаты (сработал/не сработал) последних N переходов во всей программе. Прежде чем предсказать очередной переход, процессор смотрит на этот «шаблон» из последних событий и ищет, не встречался ли ему такой шаблон раньше.
Турнирные предсказатели
Это высшая лига предсказания. Инженеры осознали, что не существует одного идеального алгоритма. Одни переходы лучше предсказываются по их локальной истории, а другие — по глобальной. Турнирный предсказатель — это мета-механизм. Внутри него живут и работают параллельно два разных предсказателя (например, один на основе локальной истории, другой — на основе глобальной).
Для каждого перехода процессор ведёт отдельный счётчик, который определяет, какой из двух предсказателей был для этого перехода точнее в недавнем прошлом. И в следующий раз для принятия окончательного решения используется прогноз того «эксперта», который зарекомендовал себя лучше для этого конкретного случая.
Именно такие гибридные, самообучающиеся и адаптивные системы позволяют современным процессорам, от Intel до AMD и Apple, достигать феноменальной точности предсказания в 99% и выше, сводя простои конвейера к абсолютному минимуму.
Вместо заключения
В общем, вот мы и познакомились поближе с предсказателем переходов. Если убрать всю сложность, это просто очень умная система, которая не даёт процессору простаивать. Без него любой современный компьютер напоминал бы водителя, который на каждом перекрёстке полностью останавливается, чтобы подумать, куда же ему свернуть.
Технология прошла путь от примитивных правил вроде «всегда поворачивай направо» до сложных алгоритмов, которые учатся на своих ошибках и помнят, что в этом конкретном месте обычно нужно повернуть налево. Сначала инженеры научили процессор запоминать результат одного последнего перехода, потом — учитывать историю, и в итоге пришли к гибридным системам, которые сами выбирают самый точный способ предсказания для каждой ситуации.
Благодаря этой незаметной работе, точность предсказаний в современных процессорах достигает 99%. Это значит, что когда вы листаете ленту или играете в игру, процессор почти всегда заранее знает, какая команда будет следующей, и не тратит драгоценные такты на пустые ожидания. В следующий раз, когда приложение мгновенно отреагирует на ваше действие, можете мысленно сказать спасибо крошечному предсказателю.
P.S. Статья писалась для рядового читателя, который хочет на понятийном уровне узнать о том, что из себя представляет предсказатель ветвлений. Поэтому (и потому что автор тоже не эксперт) в статье использовано много упрощений. За более подробными и точными сведениями о работе предсказателя вы сами знаете, куда обращаться :)