[Перевод] Гениальность микропроцессоров RISC-V

image


Войны между RISC и CISC, проходившие в конце 1990-х, уже давно отгремели, и сегодня считается, что разница между RISC и CISC совершенно не имеет значения. Многие заявляют, что наборы команд несущественны.

Однако на самом деле наборы команд важны. Они накладывают ограничения на типы оптимизаций, которые можно легко добавлять в микропроцессор.

Недавно я подробнее изучил информацию об архитектуре набора команд (instruction-set architecture, ISA) RISC-V и вот некоторые из аспектов, которые по-настоящему впечатлили меня в ISA RISC-V:

  1. Это небольшой и простой в изучении набор команд RISC. Очень предпочтителен для тех, кому интересно получать знания о микропроцессорах.
  2. Благодаря своей простоте, открытости и связи с университетскими профессорами он с большой вероятностью будет доминировать как архитектура, выбираемая для обучения процессорам в вузах.
  3. Его продуманная структура позволяет разработчикам CPU создавать высокопроизводительные микропроцессоры на основе ISA RISC-V.
  4. Благодаря отсутствую лицензионных отчислений и нацеленности на простую аппаратную реализацию увлечённый любитель может, в принципе, создать за приемлемое время собственную конструкцию процессора RISC-V.


Когда я начал понимать RISC-V лучше, то осознал, что RISC-V оказался радикальным возвратом к тому, что многие считали давно прошедшей эпохой вычислений. С точки зрения конструкции, RISC-V подобен перемещению на машине времени к классическому Reduced Instruction Set Computer (RISC, «компьютеру с набором коротких команд») начала 80-х и 90-х.

В последние годы многие заявляли, что разделение на RISC и CISC больше не имеет смысла, поскольку в процессоры RISC наподобие ARM добавили так много команд, и при этом многие из них довольно сложны, что на текущем этапе это скорее гибрид, чем чистый процессор RISC. Похожие рассуждения применялись и к другим процессорам RISC, например, PowerPC.

RISC-V же, напротив, является действительно «хардкорным» представителем процессоров RISC. Если вы почитаете в Интернете обсуждения RISC-V, то найдёте людей, утверждающих, что RISC-V был разработан какими-то олдскульными RISC-радикалами, отказывающимися двигаться в ногу со временем.

Бывшая инженер ARM Эрин Шеперд несколько лет назад написала интересную критику RISC-V:

ISA RISC-V слишком стремился к минимализму. В нём есть сильный упор на минимизацию количества команд, нормализацию кодирования и т.д. Это стремление к минимализму привело к ложным ортогональностям (например, использованию одной и той же команды для ветвления, вызовов и возвратов) и требованию избыточных команд, влияющих на плотность кода с точки зрения размера и количества команд.


Вкратце приведу немного контекста. Малый размер кода даёт преимущество в производительности, поскольку так проще хранить выполняемый код внутри высокоскоростного кэша процессора.

Критика здесь заключается в том, что проектировщики RISC-V слишком сосредоточились на обеспечении малого набора команд. В конце концов, это ведь одна из исходных целей RISC.

По словам Эрин, следствием этого стало то, что реальной программе для выполнения задач потребуется гораздо больше команд, то есть она займёт больше места в памяти.

Традиционно долгие годы считалось, что в процессор RISC нужно добавить больше команд, чтобы он стал более похожим на CISC. Идея заключается в том, что более специализированные команды могут заменить использование множественных общих команд.

Сжатие команд и Macro-Operation Fusion


Однако в архитектуре процессоров присутствуют две инновации, из-за которых эта стратегия добавления более сложных команд во многих смыслах оказывается избыточной:

  • Сжатые команды — команды сжимаются в памяти и распаковываются на первой стадии процессора.
  • Macro-operation Fusion — две или несколько простых команд считываются процессором и сливаются в одну более сложную команду.


На самом деле, в ARM уже используются обе эти стратегии, а в процессорах x86 применяется вторая, поэтому RISC-V не проделывает здесь каких-то новых трюков.

Однако тут есть тонкость: RISC-V получает из этих двух стратегий гораздо больше выгод по двум важным причинам:

  1. Сжатые команды были добавлены изначально. В других архитектурах, например, в ARM, об этом подумали позже, и прикрутили их довольно поспешным образом.
  2. Здесь оправдывает себя одержимость RISC малым количеством уникальных команд. Для добавления сжатых команд просто остаётся больше места.


Второй пункт требует некоторых пояснений. В архитектурах RISC команды обычно имеют ширину 32 бита. Эти биты нужно использовать для кодирования различной информации. Допустим, у нас есть такая команда (после точки с запятой идут комментарии):

ADD x1, x4, x8    ; x1 ← x4 + x8


Она складывает содержимое регистров x4 и x8, сохраняя результат в x1. Требуемое для кодирования этой команды количество битов зависит от количества имеющихся регистров. У RISC-V и ARM есть 32 регистра. Число 32 можно выразить 5 битами:

2⁵ = 32


Так как в команде нужно указать три разных регистра, то для кодирования операндов (входящих данных для операции сложения) требуется в сумме 15 бит (3 × 5).

Следовательно, чем больше возможностей мы хотим поддерживать в наборе команд, тем больше битов мы займём из доступных нам 32 бит. Разумеется, мы можем перейти к 64-битным командам, но при этом потратится слишком много памяти, а значит, пострадает производительность.

Агрессивно стремясь к сохранению малого количества команд, RISC-V оставляет больше места для добавления битов, обозначающих, что мы используем сжатые команды. Если процессор видит, что в команде заданы определённые биты, то он понимает, что её нужно интерпретировать как сжатую.

Это означает, что вместо засовывания внутрь 32 бит одной команды мы можем уместить две команды по 16 бит шириной каждая. Естественно, не все команды RISC-V можно выразить в 16-битном формате. Поэтому подмножество 32-битных команд выбирается на основании их полезности и частоты использования. Если несжатые команды могут получать 3 операнда (входящих данных), то сжатые команды — только 2 операнда. То есть сжатая команда ADD будет выглядеть так:

C.ADD x4, x8     ; x4 ← x4 + x8


В ассемблерном коде RISC-V используется префикс C., сообщающий, что команду нужно преобразовать ассемблером в сжатую команду.

По сути, сжатые команды уменьшают количество операндов. Три регистра-операнда заняли бы 15 бит, оставив на указание операции всего 1 бит! Таким образом, при использовании двух операндов для указания опкода (выполняемой операции) у нас остаётся 6 бит.

На самом деле это близко к тому, как работает ассемблер x86, когда зарезервировано недостаточно битов для использования трёх регистров-операндов. Процессор x86 при этом тратит биты, чтобы позволить, например, команде ADD считывать входящие данные и из памяти, и из регистров.

Однако истинную выгоду мы получаем, объединив сжатие команд с Macro-operation fusion. Когда процессор получает 32-битное слово, содержащее две сжатые 16-битные команды, он может слить их в одну более сложную команду.

Звучит, как чушь — мы что, вернулись к тому, с чего начинали?

Нет, поскольку мы минуем необходимость заполнения спецификации ISA кучей сложных команд (то есть стратегии, которой придерживается ARM). Вместо этого мы, по сути, выражаем целое множество сложных команд косвенно, через различные сочетания простых команд.

В обычных условиях Macro-fusion вызвало бы проблему: хотя две команды заменяются одной, они всё равно занимают в два раза больше памяти. Однако при сжатии команд мы не занимаем никакого лишнего места. Мы пользуемся преимуществами обеих архитектур.

Давайте рассмотрим один из примеров, приведённых Эрин Шеперд. В своей критической статье о ISA RISC-V она показывает простую функцию на C. Чтобы было понятнее, я взял на себя смелость переписать её:

int get_index(int *array, int i) { 
    return array[i];
}


На x86 это скомпилируется в следующий ассемблерный код:

mov eax, [rdi+rsi*4]
ret


При вызове функции в языке программирования аргументы обычно передаются функции в регистре согласно установленному порядку, который зависит от используемого набора команд. На x86 первый аргумент помещается в регистр rdi, второй — в rsi. По стандарту возвращаемые значения должны помещаться в регистр eax.

Первая команда умножает содержимое rsi на 4. Он содержит переменную i. Почему умножает? Потому что array состоит из элементов integer, разделённых друг от друга 4 байтами. Следовательно, третий элемент массива находится в смещении 3 × 4 = 12 байт.

Далее мы прибавляем это к rdi, который содержит базовый адрес array. Это даёт нам окончательный адрес i-того элемента array. Мы считываем содержимое ячейки памяти по этому адресу и сохраняем его в eax: задача выполнена.

На ARM всё происходит похожим образом:

LDR r0, [r0, r1, lsl #2]
BX  lr                    ;return


Здесь мы не умножаем на 4, а сдвигаем регистр r1 на 2 бита влево, что эквивалентно умножению на 4. Вероятно, это более верное описание и того, что происходит на x86. Сомневаюсь, что можно умножать на что-либо, не являющееся кратным 2, поскольку умножение — это довольно сложная операция, а сдвиг малозатратен и прост.

Из моего описания x86 об остальном можно только догадываться. Теперь давайте перейдём к RISC-V, где начинается настоящее веселье! (точкой с запятой начинаются комментарии)

SLLI a1, a1, 2     ; a1 ← a1 << 2
ADD  a0, a0, a1    ; a0 ← a0 + a1
LW   a0, a0, 0     ; a0 ← [a0 + 0]
RET


На RISC-V регистры a0 и a1 являются просто псевдонимами x10 и x11. Именно в них помещаются первый и второй аргументы вызова функции. RET — это псевдокоманда (сокращение):

JALR x0, 0(ra)     ; sp ← 0 + ra
                   ; x0 ← sp + 4  ignoring result


JALR выполняет переход к адресу, хранящемуся в ra, который относится к адресу возврата. ra — это псевдоним x1.

И всё это выглядит совершенно ужасно, правда? Вдвое больше команд для такой простой и часто используемой операции, как выполнение поиска по индексу в таблице и возврат результата.

Это действительно выглядит плохо. Именно поэтому Эрин Шеперд чрезвычайно критически отнеслась к проектировочным решениям, сделанным разработчиками RISC-V. Она пишет:

Упрощения RISC-V делают более простым декодер (т.е. фронтенд процессора), однако за это приходится расплачиваться бо́льшим количеством команд. Однако масштабирование ширины конвейера — это сложная задача, в то время как декодирование немного (или сильно) необычных команд хорошо изучено (основные сложности возникают, когда определение длины команды нетривиально — из-за своих бесконечных префиксов особо запущенным случаем является x86).


Однако благодаря сжатию команд и macro-op fusion можно изменить ситуацию к лучшему.

C.SLLI a1, 2      ; a1 ← a1 << 2
C.ADD  a0, a1     ; a0 ← a0 + a1
C.LW   a0, a0, 0  ; a0 ← [a0 + 0]
C.JR   ra


Теперь команды занимают ровно столько же места в памяти, что и пример для ARM.

Так, а теперь давайте выполним Macro-op fusion!

Одно из условий RISC-V для разрешения слияния операций в одну — это совпадение целевого регистра. Это условие выполняется для команд ADD и LW (load word, «загрузить слово»). Поэтому процессор превратит их в одну команду.

Если бы это условие выполнялось и для SLLI, то мы могли бы слить в одну все три команды. То есть процессор бы увидел нечто, напоминающее более сложную команду ARM:

LDR r0, [r0, r1, lsl #2]


Но почему мы не могли прописать эту сложную макро-операцию непосредственно в коде?

Потому что в ISA нет поддержки такой макро-операции! Вспомним, что у нас есть ограниченное количество битов. Тогда сделаем команды длиннее! Нет, это займёт слишком много памяти и быстрее переполнит драгоценный кэш процессора.

Однако если вместо этого мы будем изготавливать эти длинные полусложные команды внутри процессора, то никаких проблем не возникает. У процессора никогда не бывает одновременно в наличии более нескольких сотен команд. Поэтому если мы потратим на каждую команду, допустим, 128 бит, то это не создаст затруднений. Кремния по-прежнему будет хватать на всё.

Когда декодер получает обычную команду, он обычно превращает её в одну или несколько микро-операций. Такие микро-операции и есть команды, с которыми на самом деле работает процессор. Они могут быть очень широкими и содержат множество дополнительной полезной информации. Приставка «микро» звучит иронично, ведь они оказываются шире. Однако на самом деле «микро» означает, что они имеют ограниченное количество задач.

Macro-operation fusing немного переворачивает работу декодера вниз головой: вместо превращения одной команды в несколько микро-операций, мы берём много операций и превращаем их в одну микро-операцию.

То есть происходящее в современном процессоре может выглядеть довольно странно:

  1. Сначала он объединяет две команды в одну с помощью сжатия.
  2. Затем он разделяет их на две с помощью распаковки.
  3. Далее комбинирует их обратно в одну операцию при помощи macro-op fusion.


Другие же команды могут оказаться разделёнными на несколько микро-операций, а не быть слиты. Почему же некоторые команды сливаются, а другие разделяются? Есть ли в этом безумии система?

Ключевым аспектом перехода к микро-операциям является нужный уровень сложности:

  • Не слишком сложные, потому что в противном случае они не смогут завершиться за фиксированное количество тактов, выделенных на каждую команду.
  • Не слишком простые, потому что иначе мы просто потратим ресурсы процессора впустую. Выполнение двух микро-операций займёт в два раза больше времени, чем выполнение всего одной.


Всё началось с процессоров CISC. Intel стала разделять свои сложные команды CISC на микро-операции, чтобы их проще было уместить в конвейеры процессоров, как команды RISC. Однако в последующих конструкциях разработчики осознали, что многие команды CISC можно слить в одну умеренно сложную команду. Если команд для выполнения становится меньше, то работа будет закончена быстрее.
Мы обсудили множество подробностей, поэтому сейчас вам, должно быть, трудно понять, в чём смысл всех этих трудов. Зачем нужны все эти сжатия и слияния? Похоже, что из-за них выполняется много лишней работы.

Во-первых, сжатие команд совершенно не походит на сжатие zip. Слово «сжатие» немного неверное, потому что мгновенное сжатие или распаковка команды выполняются абсолютно просто. На это не тратится времени.

То же самое относится к macro-operation fusion. Хотя этот процесс может казаться сложным, подобные системы уже используются в современных микропроцессорах. Поэтому затраты, которые добавляет вся эта сложность, уже были оплачены.

Однако в отличие от проектировщиков ARM, MIPS и x86, приступая к проектированию своего ISA, создатели RISC-V знали о сжатии команд и macro-ops fusion. Благодаря различным тестам с первым минимальным набором команд они сделали два важных открытия:

  1. Программы RISC-V обычно занимают примерно столько же или меньше места в памяти, чем любая другая процессорная архитектура. В том числе и x86, который должен эффективно использовать память, учитывая, что это ISA CISC.
  2. Ему требуется выполнять меньше микро-операций, чем другим ISA.


По сути, спроектировав базовый набор команд с учётом использования fusion, они смогли сливать достаточное количество команд для того, чтобы процессору для любой программы приходилось выполнять меньше микро-операций, чем процессорам-конкурентам.

Это заставило коллектив разработчиков RISC-V удворить усилия по реализации macro-operation fusion как фундаментальной стратегии RISC-V. В руководстве по RISC-V есть множество примечаний о том, с какими операциями можно выполнять слияние. Также в него внесены правки, упрощающие слияние команд, встречающихся в частых паттернах.

Благодаря малому ISA его проще изучать студентам. А это означает, что изучающему процессорные архитектуры студенту проще спроектировать собственный процессор, работающий на командах RISC-V. Стоит помнить, что и сжатие команд, и macro-op fusion использовать необязательно.

RISC-V имеет небольшой фундаментальный набор команд, реализация которого обязательна. Однако все остальные команды реализуются как части расширений. Сжатые команды — это просто дополнительное расширение.

Macro-op fusion — это просто оптимизация. Она не меняет поведения в целом, а поэтому её необязательно реализовывать в собственном процессоре RISC-V.


RISC-V взял всё, что мы знаем сегодня о современных процессорах, и использовал эти знания в проектировании процессоров ISA. Например, мы знаем, что:

  • Сегодня у процессорных ядер есть сложная система прогнозирования ветвления.
  • Процессорные ядра суперскалярны, то есть выполняют множество команд параллельно.
  • Для обеспечения суперскалярности используется выполнение команд с изменением очерёдности (Out-of-Order execution).
  • Они имеют конвейеры.


Это означает, что такие особенности, как поддерживаемое ARM условное выполнение, больше не требуется. Поддержка этой функции в ARM отъедает биты от формата команд. RISC-V может сэкономить эти биты.

Изначально условное выполнение создавалось для того, чтобы избегать ветвлений, потому что они плохо влияют на конвейеры. Для ускорения работы процессора он обычно заранее получает следующие команды, чтобы сразу после выполнения предыдущей на первой стадии процессора можно было подхватить следующую.

При условном ветвлении мы не можем заранее знать, где будет следующая команда, когда начинаем заполнять конвейер. Однако суперскалярный процессор может просто выполнять обе ветви параллельно.

Именно из-за этого RISV-C не имеет и регистров состояния, ведь они создают зависимости между командами. Чем более независима каждая команда, тем проще выполнять её параллельно с другой командой.

По сути, стратегия RISC-V заключается в том, что мы можем сделать ISA как можно более простым, а минимальную реализацию процессора RISC-V как можно более простой без необходимости принятия конструкторских решений, из-за которых невозможно будет создать высокопроизводительный процессор.


На правах рекламы


Наша компания предлагает серверы не только с CPU от Intel, но и серверы с процессорами AMD EPYC. Как и для других типов серверов, огромный выбор операционных систем для автоматической установки, есть возможность установить любую ОС с собственного образа. Попробуйте прямо сейчас!

8p3vz47nluspfyc0axlkx88gdua.png

© Habrahabr.ru