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

в 16:48, , рубрики: risc-v, ассемблер, Процессоры

Изначально я написала этот документ несколько лет назад, будучи инженером по проверке ядра исполнения команд (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-разрядных значений. Это приводит к ненужному переключению или специальному размещению верхней половины регистров. Более оптимально использовать нулевое расширение (поскольку оно уменьшает число переключений и обычно его можно оптимизировать путём отслеживания «нулевого» бита, когда верхняя половина, как известно, равна нулю)
  • Умножение опционально. Хотя быстрые блоки перемножения занимают ненулевую площадь на крошечных реализациях, но можно широко использовать существующий 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js