Как стать автором
Обновить

Недостатки RISC-V

Assembler *Процессоры
Перевод
Автор оригинала: Erin Alexis Owen Shepherd
Изначально я написала этот документ несколько лет назад, будучи инженером по проверке ядра исполнения команд (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, RsR1
    • (Странный переход: RdR0, RdR1)
  • Кодирование с переменной длиной поля записи не самосинхронизируется (такое часто встречается — например, аналогичная проблема у x86 и Thumb-2, — но это вызывает различные проблемы как с реализацией, так и с безопасностью, например, возвратно-ориентированное программирование, то есть атаки ROP)
  • RV64I требует расширения знака для всех 32-разрядных значений. Это приводит к тому, что верхнюю половину 64-битных регистров становится невозможно использовать для хранения промежуточных результатов, что ведёт к ненужному специальному размещению верхней половины регистров. Более оптимально использовать расширение нулями (поскольку оно уменьшает число переключений и обычно его можно оптимизировать путём отслеживания «нулевого» бита, когда верхняя половина, как известно, равна нулю)
  • Умножение опционально. Хотя быстрые блоки перемножения могут занимать довольно существенную площадь на крошечных кристаллах, но всегда можно использовать чуть более медленные схемы, которые активно используют существующий ALU для многократных циклов умножения.
  • У LR/SC строгое требование к поступательному продвижению для ограниченного подмножества применений. Хотя это ограничение довольно жёсткое, оно потенциально создаёт некоторые проблемы для небольших реализаций (особенно без кэша)
    • Это кажется заменой инструкции CAS, см. комментарий ниже
  • Биты закрепления в памяти (sticky bits) FP и режим округления находятся в одном регистре. Это требует сериализации канала FP, если выполняется операция RMW для изменения режима округления
  • Инструкции FP кодируются для 32, 64 и 128-битной точности, но не 16-битной (что значительно чаще встречается в аппаратном обеспечении, чем 128 бит)
    • Это можно легко исправить: код размерности 0b10 свободен
    • Обновление: в версии 2.2 появился десятичный заполнитель, но нет заполнителя половинной точности. Уму непостижимо.
  • То, как значения FP представлены в файле регистра FP, не определено, но наблюдаемо (через load/store)
    • Авторы эмуляторов вас возненавидят
    • Миграция виртуальных машин может стать невозможной
    • Обновление: версия 2.2 требует более широких значений NaN-boxing

Плохо


  • Отсутствуют коды условий, а вместо них используются инструкции compare-and-branch. Это не проблема сама по себе, но последствия неприятные:
    • Уменьшение пространства кодирования в условных переходах из-за необходимости кодирования одного или двух спецификаторов регистров
    • Нет условного выбора (полезно для очень непредсказуемых переходов)
    • Нет сложения с переносом / вычитания с переносом или заимствованием
    • (Обратите внимание, что это всё равно лучше, чем наборы команд, которые пишут флаги в регистр общего назначения, а затем переходят на полученные флаги)
  • Кажется, что высокоточные счётчики (аппаратных циклов) требуются в непривилегированной 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-битных целочисленных регистров нельзя составить 64-битное число двойной точности с плавающей точкой, придётся сначала записать промежуточное значение в память и загрузить его в регистровый файл оттуда
  • Например, у 32-битной инструкция ADD в RV32I и 64-битной ADD в RVI64 одинаковые кодировки, а в RVI64 добавляется ещё и другая кодировка ADD.W. Это ненужное усложнение для процессора, который реализует обе инструкции — было бы предпочтительнее вместо этого добавить новую 64-битную кодировку.
  • Нет инструкции MOV. Мнемокод команды MV транслируется ассемблером в инструкцию MV rD, rS-->ADDI rD, rS, 0. Высокопроизводительные процессоры обычно и так оптимизируют инструкции MOV, широко задействуя при этом переупорядочивание команд. В качестве канонической формы команды MV в RISC-V была выбрана инструкция с непосредственным 12-битовым операндом.
    • При отсутствии MOV инструкция ADD rD, rS, r0 фактически становится предпочтительнее канонической MOV, поскольку её проще декодировать, а операции с нулевым регисторм (r0) в CPU обычно оптимизированы

Ужасно


  • JAL тратит 5 бит на кодирование регистра связи, который всегда равен R1 (или R0 для переходов)
    • Это означает, что RV32I использует 21-битные смещения ветвей (branch displacement). Это недостаточно для больших приложений — например, веб-браузеров — без использования нескольких последовательностей команд и/или «островов ветвей» (branch islands)
    • Это ухудшение по сравнению с версией 1.0 архитектуры команд!
  • Несмотря на большие усилия на равномерное кодирование, инструкции 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-пространстве, так что бинарники остаются обратно совместимыми)
Теги:
Хабы:
Всего голосов 42: ↑39 и ↓3 +36
Просмотры 25K
Комментарии Комментарии 53