Не так давно одному из авторов этого поста довелось интервьюировать Майка Кларка, который, в частности, сказал: “…вот увидите, какой принципиальный прогресс готовит нам будущее в виде процессора Zen 6, хотя, на самом деле, все условия для этого были заложены ещё на этапе Zen 5». На той же конференции по архитектуре, посвящённой Zen 5 выступил и Марк Пейпермастер, генеральный технологический директор AMD, высказавшийся так: «Zen 5 — это основательная переработка всей архитектуры Zen». Всё дело в многочисленных и принципиальных изменениях в устройстве ядра, которые обогатили процессор именно в этой версии.
Не будет преувеличением сказать, что наиболее значительным из этих изменений является новейший 2-опережающий блок предсказания ветвлений, архитектурная новинка, корни которой уходят в одну научную статью, написанную тридцать лет назад. Однако, прежде, чем углубиться в изучение этой старой и одновременно свежей идеи, давайте кратко вспомним, в чём заключается функция предсказателей ветвлений, и почему они настолько важны в современных микропроцессорных ядрах.
С тех пор, как компьютеры стали оперировать программами, хранимыми в программируемой оперативной памяти с произвольным доступом, любая архитектура разделилась на фронтенд, выбирающий инструкции, и бэкенд, отвечающий за выполнение этих операций. Кроме того, фронтенд должен обеспечивать возможность произвольно перемещать точку выполнения текущей программы, тем самым реализуя такой базовый функционал, как условные сравнения, циклическое выполнение и создание субпроцедур.
Если бы процессор мог просто выполнять в унисон как одно целое такую задачу: выбрать инструкцию, выполнить её и выбрать местоположение следующей инструкции — то здесь было бы практически нечего обсуждать. Но из-за того, что постоянно растут запросы на повышение производительности, приходится делать так, что процессоры выполняют всё больше операций в единицу времени и на некоторый электронный блок. Именно так частота процессора постепенно выросла от 5 кГц у ENIAC до 5+ ГГц у некоторых современных процессоров, например, Zen 5. В такой ситуации никак не обойтись без конвейеризации логики. Фактически, процессору приходится параллельно поддерживать постепенно выполняемые промежуточные состояния тех операций, которые с логической точки зрения являются хронологически несовпадающими.
Держать данный конвейер заполненным очень сложно уже в силу наличия условных переходов в программе. Откуда фронтенду знать, какие именно инструкции начинать выбирать, декодировать и диспетчеризовать, если условие перехода может находиться в добром десятке тактовых циклов от той точки, где завершается вычисление? Проблему представляют даже безусловные переходы, для которых статически известен целевой адрес — если на выборку и декодирование инструкции требуется сверх одного звена конвейера.
Два радикальных решения этой проблемы таковы: либо просто дожидаться, пока не наступит такая необходимость, либо постараться как можно точнее угадывать, что делать дальше, и предусмотреть при этом размотку кода именно из той точки, в которой обнаружена ошибка. Размотка неверных предположений должна строиться так: опустошаем конвейер с задачами, который привёл нас к неверной версии и перезапускаем задачу с последней достоверно известной корректной точки. Стопор программы, с которым мы сталкиваемся при переходе в очередную ветку по условию, практически неустраним. Масштаб проблемы будет пропорционален количеству звеньев конвейера между выборкой инструкции и интерпретацией условия ветки (завершением этой операции). Учитывая это и сопутствующую необходимость не растрачивать полосу передачи сигнала, у нас практически не остаётся иного выхода, кроме как пытаться максимально точно угадывать последовательности инструкций в программе.
На минутку вообразите себя курьером по доставке еды, и у вас нет ни карты, ни GPS-навигации. Всё, что вам остаётся – на лету ориентироваться по подсказкам коллег, которые сидят в кузове грузовика. Вдобавок представьте, что стёкла у вас в кабине тонированы так, что рассмотреть в них что-либо невозможно, а ваши товарищи успевают сказать «поворачивай!» примерно секунд через сорок пять после того, как вы уже проскочили перекрёсток, которого, кстати, даже не видели. В таком случае вы получите какое-то представление о трудностях, с которыми приходится иметь дело механизму выборки инструкций в конвейеризованном процессоре (возможно, вы даже посочувствуете ему). Искусство предсказания веток — это целая вселенная стратегий, позволяющих сбавить темп, в котором этот несчастный курьер вынужден притормаживать и выруливать.
Существуют упрощённые стратегии — например, всегда предпринимать короткие переходы назад (словно поворачиваешь по кольцевой развязке). Опыт показывает, что такая тактика действительно более выигрышная, чем всегда выбирать из памяти инструкцию с максимальным из оставшихся адресов (словно всегда едешь прямо). Правда, если разрешается поддерживать некоторый минимальный объём состояния, то в реальных программах удаётся достичь гораздо более качественных результатов. Если пока аналогия с ничего не видящим водителем грузовика ещё не кажется вам слишком натянутой, представьте, что водитель ведёт в блокноте небольшой список заметок: на какие съезды он поворачивал, какие пропускал. Также он от руки делает наброски — как расположены те участки дороги, которые он проезжал в последние несколько минут, какие перекрёстки миновал. В нашем случае эти вещи аналогичны истории ветвлений и записи адресов. Структуры размером в десятки килобайт позволяют довести процент успешного предсказания ветвлений выше 90%. В этой статье мы не будем пытаться охватить колоссальное пространство исследований и набор имеющихся в этой области коммерческих решений, но в данном случае ценно хотя бы начинать понимать, чем обоснованы все эти решения.
Аннотированная планировка из презентации AMD «Zen 4” – The AMD 5nm 5.7 GHz x86-64 Microprocessor Core» с конференции ISSCC за 2023 год. Фронтенд забирает половину всего кэша L1 и области целочисленной логики, причём, логика предсказания ветвлений и ведение состояний этого механизма — самая большая статья расходов в любой передней подсистеме.
Теперь знакомьтесь с нашим фронтменом: 2-опережающий предсказатель
ветвлений в процессоре Zen 5.
Предложение о реализации 2-опережающего предсказателя ветвлений, который воплотился в процессоре Zen 5, впервые появилось ещё в начале 90-х. Ещё тогда существовал настоящий вызов: как горизонтально масштабироваться за пределы полосы шириной в восемь бит, предусмотренной в архитектуре процессора. Так и зашла речь о 2-опережающем предсказателе ветвлений как об одном из методов, предлагавшемся в академических кругах с целью выжимать всё больше производительности из одноядерного процессора.
Но по мере того, как производители коммерческих процессоров постепенно стали переходить от одноядерных моделей к многоядерным, академические исследования также сосредотачивались на как можно более эффективном повышении производительности на единицу полезной площади процессора. Наиболее существенной разработкой в этой области был предсказатель TAGE. Он значительно превосходит в эффективности более ранние механизмы предсказания ветвлений, поэтому именно в сторону его совершенствования велись академические исследования.
Но с появлением логических узлов, позволяющих смежно располагать всё больше и больше транзисторов, а также с переходом от двухъядерных и четырёхъядерных процессоров к машинам с сотнями ядер, приспособленных для внеочередного исполнения инструкций (out of order), всё больше внимания стали уделять повышению производительности единственного ядра, а не развивать масштабирование. Поэтому, хотя некоторые из этих идей старше меня, они стали вновь актуализироваться по мере того, как разные компании изыскивают способы повысить производительность единственного ядра.
Стоит обсудить один аспект x86, позволяющий неизмеримо выигрывать от использования 2-опережающего предсказателя ветвлений, нежели от применения любой другой ISA (архитектуры набора команд). Архитектуры, в которых используются инструкции фиксированной длины, например, 64-разрядная Arm, элементарно позволяют декодировать произвольные подмножества в кэш-линии некоторой инструкции. Причём, эта задача распараллеливается просто путём репликации логики декодера с сегментированием входных данных по гарантированным байтовым границам инструкций. На совершенно противоположном краю спектра находится процессор x86, в котором требуется линейно разбирать инструкции по байтам, выявляя таким образом, где именно расположена граница между текущей и следующей инструкцией. При конвейеризации (когда инструкция декодируется не на всю длину, а частично — в первую очередь определяются префиксы) распараллеливание можно считать пусть и не дешёвой, но подъёмной операцией. Поэтому полоса декодирования шириной 4 бит на протяжении многих лет оставалась типичной для ядер x86, ориентированных на высокую производительность.
Поступательно повышая логическую плотность при помощи новых fab-узлов, в конце концов, удалось довести до коммерческой целесообразности механизм декодирования с шириной полосы 6 бит, как в Golden Cove. При этом расходы на площадь и мощность при работе с монолитной параллельной архитектурой x86 определённо растут сверхлинейно с увеличением ширины, причём, здесь и близко не просматривается лёгкого пути к дальнейшей экспансии. Возможно, следует благодарить Intel и AMD за то, что для применяемого в приложениях типичного целочисленного кода характерна значительная плотность ветвлений: примерно по одной ветке на пять-шесть инструкций. Поэтому не так остро стоит вопрос о дальнейшем развитии распараллеленных декодировщиков.
Более всего прочего (в качестве предохранительного клапана) для фронтенда x86 требуется возможность определять, какая часть декодирования по определению не распараллеливается, т.е., определять границы инструкций. Если бы только нашёлся какой-то способ легко переноситься вперёд в процессе декодирования, и при этом как по волшебству гарантированно приземляться прямо на границе инструкций…
Назад в будущее... как в 1990-х
Начиная со статьи “Multiple-block ahead branch predictors” by Seznec et al., постепенно излагается, как и зачем реализовывать 2-опережающий предсказатель ветвлений, а также детали этой реализации.
Заглянув в статью, вы убедитесь, что реализовать предсказатель ветвлений, который бы позволял оперировать множеством веток на цикл, совсем не так просто, как сделать предсказатель, способный учитывать развитие по многим веткам. Чтобы была возможность использовать 2-опережающий предсказатель ветвлений на полную мощность, не выдавая запредельных требований, Seznac et al. рекомендуют переориентировать выбор инструкций в две очереди.
Взглянув на Zen 5, видим, что в данном случае AMD именно так и сделала — переориентировала в две линии выборку инструкций и операционный кэш. Теперь в AMD применяются два конвейера выборки инструкций по 32 байта на цикл, взаимодействующие с 32-килобайтным кэшем инструкций L1. Каждый из этих конвейеров подаёт информацию в собственный декодирующий кластер шириной 4 бита. Кэш операций теперь имеет сдвоенную структуру шириной 6 бит, через которую можно подавать в очередь операций до 12 операндов.
Кроме того, Seznac et al. также рекомендуют портировать на сдвоенную версию и буфер адресов переходов (BTB). Именно тем, что на сдвоенную версию портирован L1 BTB, объясняются огромный массив в 16K записей, к которым может обращаться L1 BTB. Что касается L2 BTB, он не столь велик, как L1 BTB и содержит всего 8K записей, но AMD использует его примерно по тому же принципу, по которому задействовался бы кэш вытеснения. Таким образом, те записи, которые выбрасываются из L1 BTB, оказываются в L2 BTB.
С учётом всех этих изменений Zen 5 справляется с двумя ветвлениями на цикл в пределах несплошного блока инструкций.
Такой подход должен ограничить попадания по полосе выборки инструкций, когда Zen 5 попадает по состоявшемуся переходу. Кроме того, AMD получает возможность прогнозировать переходы с запасом на два шага вперёд.
Zen 5 может заглядывать в поток инструкций ещё далее, чем на два ближайших ветвления. В результате у Zen 5 появляется 3 окна прогнозирования, и все эти окна дают полезную информацию при подготовке инструкций по декодированию. Вот как это работает: поле длиной 5 бит прикрепляется ко второму окну предсказаний. Тем самым предотвращается избыточная подписка на ресурсы декодирующего или операционного кэша. Поле в 5 бит даже короче, чем даст вам указатель в начале третьего предсказательного окна. Одно из достоинств такой ситуации — если третье окно пересечёт границу кэш-линии, то в индексе поиска предсказаний не придётся хранить дополнительное состояние для следующего цикла. Недостаток же в том, что, если третье предсказательное окно окажется в той же кэш-линии, что первое и второе, такое половинчатое третье окно будет не столь эффективным, как полноценное третье окно.
Теперь, при двух активных потоках в Zen 5 удаётся статически разграничить кластеры декодирования и сопутствующие им конвейеры выборки. Таким образом, чтобы Zen 5 работал как двухъядерный механизм выборки, ему придётся выбирать инструкции одновременно из кэша инструкций L1 и из кэша операций. Возможно, именно поэтому AMD перенесла на сдвоенную модель и операционный кэш — для более уверенной гарантии работоспособности двойного конвейера выборки.
Заключение
В конце концов, такой новый 2-опережающий предсказатель ветвлений приносит серьёзные перемены в семейство процессорных архитектур Zen. Это шаг вперёд, который открывает новые возможности предсказания ветвлений. Наверняка этот предсказатель ветвлений будет и далее развиваться и оттачиваться, поэтому ядро Zen будет занимать всё более выгодные позиции на фоне аналогов.
Дополнительное чтение
Если вы хотите подробнее изучить, как работают процессоры с множественной выборкой инструкций, то настоятельно рекомендуем вам изучить перечисленные ниже статьи. С их помощью мы сами основательно разобрались, как работает вся эта система:
“Multiple-block ahead branch predictors” by Seznec et al. – ASPLOS 1996
“Optimization of Instruction Fetch Mechanisms for High Issue Rates” by Conte et al. – ISCA 1995
“Out-of-Order Instruction Fetch using Multiple Sequencers” by Oberoi and Sohi – ICPP’02
“Parallelism in the Front-End” by Oberoi and Sohi – ISCA 2003