Изначально я написала этот документ несколько лет назад, будучи инженером по проверке ядра исполнения команд (execution core verification engineer) в ARM. Конечно, на моё мнение повлияла углублённая работа с исполнительными ядрами разных процессоров. Так что делайте на это скидку, пожалуйста: может, я слишком категорична.
Однако я по-прежнему считаю, что создатели RISC-V могли справиться гораздо лучше. С другой стороны, если бы я сегодня проектировала 32-или 64-разрядный процессор, то, вероятно, реализовала бы именно такую архитектуру, чтобы воспользоваться существующим инструментарием.
Статья изначально описывала набор команд RISC-V 2.0. Для версии 2.2 в ней сделаны некоторые обновления.
Оригинальное предисловие: немного личного мнения
Набор команд RISC-V доведён до абсолютного минимума. Большое внимание уделяется минимизации числа инструкций, нормализации кодирования и т. д. Это стремление к минимализму привело к ложным ортогональностям (таким как повторное использование одной и той же инструкции для переходов, вызовов и возвратов) и обязательной многословности, что раздувает и размер, и количество инструкций.
Например, вот код C:
int readidx(int *p, size_t idx)
{ return p[idx]; }
Это простой случай индексирования массива, очень распространённая операция. Так выглядит компиляция для x86_64:
mov eax, [rdi+rsi*4]
ret
или ARM:
ldr r0, [r0, r1, lsl #2]
bx lr // return
Однако для RISC-V необходим такой код:
slli a1, a1, 2
add a0, a1, a1
lw a0, a0, 0
jalr r0, r1, 0 // return
Симплификация RISC-V упрощает декодер (т. е. фронтенд CPU) за счёт выполнения большего количества инструкций. Но масштабирование ширины конвейера — сложная проблема, в то время как декодирование слегка (или сильно) нерегулярных инструкций хорошо реализуется (основная трудность возникает, когда трудно определить длину инструкции: это особенно проявляется в наборе команд x86 с многочисленными префиксами).
Упрощение набора инструкций не следует доводить до предела. Регистр со сдвигом регистровой памяти — несложная и очень распространённая инструкция в программах, а процессору очень легко её эффективно реализовать. Если процессор не способен реализовать инструкцию напрямую, то может относительно легко разбить её на составляющие; это гораздо более простая проблема, чем слияние последовательностей простых операций.
Мы должны отличать «сложные» специфические инструкции CISC-процессоров — усложнённые, редко используемые и малооэффективные инструкции — от «функциональных» инструкций, общих для процессоров CISC и RISC, которые объединяют небольшую последовательность операций. Последние используются часто и с высокой производительностью.
Посредственно
- Почти неограниченная расширяемость. Хотя это и является целью RISC-V, но это создаёт фрагментированную, несовместимую экосистему, которой придётся управлять с особой осторожностью
- Одна и та же инструкция (
JALR
) используется и для вызовов, и для возвратов и для косвенно-регистровых переходов (register-indirect branches), где требуется дополнительное декодирование для предсказания ветвей- Вызов:
Rd
=R1
- Возврат:
Rd
=R0
,Rs
=R1
- Косвенный переход:
Rd
=R0
,Rs
≠R1
- (Странный переход:
Rd
≠R0
,Rd
≠R1
)
- Вызов:
- Кодирование с переменной длиной поля записи не самосинхронизируется (такое часто встречается — например, аналогичная проблема у x86 и Thumb-2, — но это вызывает различные проблемы как с реализацией, так и с безопасностью, например, возвратно-ориентированное программирование, то есть атаки ROP)
- RV64I требует расширения знака для всех 32-разрядных значений. Это приводит к ненужному переключению или специальному размещению верхней половины регистров. Более оптимально использовать нулевое расширение (поскольку оно уменьшает число переключений и обычно его можно оптимизировать путём отслеживания «нулевого» бита, когда верхняя половина, как известно, равна нулю)
- Умножение опционально. Хотя быстрые блоки перемножения занимают ненулевую площадь на крошечных реализациях, но можно широко использовать существующий ALU для многократных циклов умножения.
- У
LR
/SC
строгое требование к поступательному продвижению для ограниченного подмножества применений. Хотя это ограничение довольно жёсткое, оно потенциально создаёт некоторые проблемы для небольших реализаций (особенно без кэша)- Это кажется заменой инструкции CAS, см. комментарий ниже
- Биты закрепления в памяти FP и режим округления находятся в одном регистре. Это требует сериализации канала FP, если выполняется операция RMW для изменения режима округления
- Инструкции
FP
кодируются для 32, 64 и 128-битной точности, но не 16-битной (что значительно чаще встречается в аппаратном обеспечении, чем 128 бит)- Это можно легко исправить: кодирование
2'b10
бесплатно - Обновление: в версии 2.2 появился десятичный заполнитель, но нет заполнителя половинной точности. Уму непостижимо.
- Это можно легко исправить: кодирование
- То, как значения FP представлены в файле регистра FP, не определено, но наблюдаемо (через load/store)
- Авторы эмуляторов вас возненавидят
- Миграция виртуальных машин может стать невозможной
- Обновление: версия 2.2 требует более широких значений NaN-boxing
Плохо
- Отсутствуют коды условий, а вместо них используются инструкции compare-and-branch. Это не проблема сама по себе, но последствия неприятные:
- Уменьшение пространства кодирования в условных переходах из-за необходимости кодирования одного или двух спецификаторов регистров
- Нет условного выбора (полезно для очень непредсказуемых переходов)
- Нет сложения с переносом / вычитания с переносом или заимствованием
- (Обратите внимание, что это всё равно лучше, чем наборы команд, которые пишут флаги в GPR, а затем переходят на полученные флаги)
- Высокоточные счётчики, по-видимому, требуются на уровне пользователя ISA. На практике, предоставление их приложениям является отличным вектором для атак по боковым каналам
- Умножение и деление являются частью одного и того же расширения, и кажется, что если одно реализовано, то и другое тоже должно быть. Умножение значительно проще, чем деление, и распространено на большинстве процессоров, а деление нет
- Нет атомарных инструкций в базе ISA. Всё более распространёнными становятся многоядерные микроконтроллеры, так что атомарные типы LL/SC совсем недорогие (для минимальной реализации одного процессора требуется только 1 бит состояния процессора)
LR
/SC
находятся в том же расширении, что и более сложные атомарные инструкции, что ограничивает гибкость для небольших реализаций- Общие атомарные инструкции (не
LR
/SC
) не включают примитивCAS
- Смысл в том, чтобы избежать необходимости в инструкции, которая читает пять регистров (
Addr
,CmpHi:CmpLo
,SwapHi:SwapLo
), но это, вероятно, наложит меньше накладных расходов на реализацию, чем гарантированное продвижение вперёдLR
/SC
, которое предоставляется в качестве замены
- Смысл в том, чтобы избежать необходимости в инструкции, которая читает пять регистров (
- Предлагаются атомарные инструкции, которые работают на 32-разрядных и 64-разрядных величинах, но не 8-ми или 16-битных
- Для RV32I нет способа передать значение DP FP между целым числом и файлами регистра FP, кроме как через память
- Например, у 32-битной инструкция
ADD
в RV32I и 64-битнойADD
в RVI64 одинаковые кодировки, а в RVI64 добавляется ещё и другая кодировкаADD.W
. Это ненужное усложнение для процессора, который реализует обе инструкции — было бы предпочтительнее вместо этого добавить новую 64-битную кодировку. - Нет инструкции
MOV
. АлиасMV
реализуется какMV rD, rS
->ADDI rD, rS, 0
. ОптимизациюMOV
обычно выполняют высокопроизводительные процессоры (особенно нестандартные). Распознавание каноническойMV
в RISC-V требует немедленного осуществления операции OR в 12 бит- При отсутствии
MOV
инструкцияMOV rD, rS, r0
фактически становится предпочтительнее каноническойMOV
, поскольку её легче декодировать, а в CPU обычно есть специальная логика для распознавания нулевого регистра
- При отсутствии
Ужасно
JAL
тратит 5 бит на кодирование регистра связи, который всегда равенR1
(илиR0
для переходов)- Это означает, что RV32I использует 21-битные смещения ветвей (branch displacement). Это недостаточно для больших приложений — например, веб-браузеров — без использования нескольких последовательностей команд и/или «островов ветвей» (branch islands)
- Это хуже, чем в первой версии ISA!
- Несмотря на большие усилия на равномерное кодирование, инструкции load/store кодируются по-разному (меняются регистр и непосредственные поля)
- Видимо, ортогональность кодирования выходного регистра была предпочтительнее ортогональности кодирования двух сильно связанных инструкций. Этот выбор кажется немного странным, учитывая, что генерация адресов более критична по времени
- Нет нагрузок со смещениями регистров (
Rbase
+Roffset
) или индексов (Rbase
+Rindex
<<Scale
). FENCE.I
подразумевает полную синхронизацию кэша инструкций со всеми предыдущими хранилищами, с ограждением (fenced) или без него. Реализациям нужно или очищать весь I$ на ограждении, или выискивать D$ и накопительный буфер (store buffer)- В RV32I чтение 64-битных счётчиков требует двукратного чтения верхней половины, сравнения и ветвления в случае переноса между нижней и верхней половиной во время операции чтения
- Обычно 32-разрядные ISA включают в себя инструкцию «чтение пары специальных регистров», чтобы избежать этой проблемы
- Нет архитектурно определённого пространства hint-кодирования, которое выполнятся как
NOP
на текущих процессорах, но имеет некоторое поведение на самых современных CPU- Типичные примеры чистых «хинтов NOP» — такие вещи, как spinlock yield
- На новых процессорах также реализованы более сложные хинты (с видимыми побочными эффектами на новых процессорах; например, инструкции проверки границ x86 кодируются в hint-пространстве, так что бинарники остаются обратно совместимыми)
Автор: m1rko