RISC-V: открытая система команд и почему это важно
В предыдущих главах трека мы установили две вещи. Первая: система команд — это контракт между железом и программой, а не описание внутренностей процессора (Система команд). Вторая: спор RISC против CISC закончился не победой одной стороны, а тем, что обе конструкции сошлись в середине — внутри современного x86 живёт RISC-подобное ядро, а «чистые» RISC обзавелись плотными кодировками и сложными операциями (RISC и CISC).
Из этого следует неудобный вывод: если контракт и реализация разделены, то владение контрактом — это отдельная переменная, не сводимая ни к скорости, ни к энергоэффективности. Кто-то решает, что можно записать в спецификацию, кому позволено её реализовать и на каких условиях. Полвека эта переменная была скучной константой: у x86 и ARM ответ был известен и менялся редко. RISC-V меняет именно её — и уже поэтому заслуживает главы, даже если бы не принёс ни одной новой инженерной идеи. Он, впрочем, принёс: несколько решений в нём — это осознанный ответ на грабли, на которые наступили MIPS, SPARC, Alpha и Itanium.
Эта глава — не рекламный проспект. Мы не будем сравнивать «что быстрее»: скорость определяется микроархитектурой и техпроцессом, а не буквами в спецификации, и к этому мы вернёмся отдельно. Мы разберём: что технически означает «открытая» система команд, как устроен базовый набор и механизм расширений, почему поля команды лежат именно на этих битах, за какие решения пришлось заплатить и чем, как выглядят привилегированные режимы и стек прошивки, и где RISC-V физически стоит в железе на сегодня.
Полезный фон, если он не прочитан: устройство процессора и путь от кода к исполнению дают вводный уровень, а основы ассемблера — регистры, стек и соглашения о вызовах, которыми мы здесь пользуемся как готовым словарём.
Что технически означает «открытая система команд»
Слово «открытый» в разговорах о RISC-V используют так небрежно, что смысл теряется. Разложим по слоям, потому что слои здесь разные и путать их дорого.
Спецификация — текст, описывающий команды, кодировки, регистры, режимы и модель памяти. У RISC-V она опубликована свободно: её можно скачать, прочитать, реализовать в железе, продать чип и не платить ни цента отчислений и не спрашивать разрешения. Это и есть единственный смысл слова «открытая».
Реализация — конкретное ядро: конвейер, кэши, предсказатель переходов. Может быть любой: полностью закрытой коммерческой IP, купленной у поставщика, или опубликованной под свободной лицензией. Открытость спецификации о реализации не говорит ничего.
Кристалл — физическое изделие. Стоит денег в любом случае: маски, верификация, физический дизайн, тестирование (об этом — Производство). Бесплатная спецификация снижает одну статью расходов из десятка, и обычно не самую крупную.
Три разные модели владения
Сравним не характеристики, а именно юридико-инженерные модели — это и есть предмет главы.
x86-64. Права размазаны между Intel и AMD через перекрёстные лицензии, накопленные за десятилетия. Появление нового независимого производителя x86 практически исключено: не потому что кодировка сложна, а потому что нет пути получить права. Следствие для инженера: архитектура развивается ровно так, как договорятся два (иногда три) участника, и точка принятия решений находится вне вашего контроля.
ARM (AArch64). Спецификация принадлежит одной компании и лицензируется. Есть два уровня: лицензия на готовое ядро (берёте Cortex-X и вставляете в свой SoC) и архитектурная лицензия (имеете право спроектировать собственное ядро, исполняющее AArch64 — так делают Apple, Qualcomm через купленную Nuvia, Fujitsu, Ampere). Обе платные, обе с условиями, обе — предмет переговоров. Модель работает и дала огромную экосистему, но точка отказа одна.
RISC-V. Спецификацию публикует некоммерческая RISC-V International; с 2020 года организация зарегистрирована в Швейцарии — этот переезд открыто объяснялся желанием не зависеть от юрисдикции одной страны. Реализовывать может кто угодно. Товарный знак защищён (нельзя назвать «RISC-V» несовместимую поделку), сама архитектура — нет.
Чего открытость не даёт
Четыре честных «нет», которые стоит проговорить сразу, чтобы дальше читать без иллюзий:
- Не даёт бесплатного железа. Стоимость чипа определяется масками, верификацией и объёмом выпуска. Порядки величин: комплект масок на зрелом техпроцессе — десятки-сотни тысяч USD, на передовом — миллионы и выше; лицензионные отчисления на этом фоне значимы, но не решающи. Открытая спецификация убирает шлагбаум, а не смету.
- Не даёт производительности. Ни одна буква в спецификации не выполняет команды. Скорость даёт конвейер, кэши, предсказатель — главы 06, 07, 08, 09.
- Не даёт совместимости автоматически. Свобода добавлять расширения — это и главная фича, и главный риск; про это будет отдельный раздел.
- Не означает «открытый исходник». Открытые ядра существуют (о них ниже), но «RISC-V» и «open-source hardware» — пересекающиеся, а не совпадающие множества.
Тогда почему это вообще важно
Аргументы, которые остаются после вычёркивания маркетинга:
- Экспериментировать можно без юридического разрешения. Университет, стартап или исследовательская группа строит ядро, публикует его, другие воспроизводят результат. Для микроархитектуры это принципиально: раньше работы сравнивали на упрощённых учебных ISA, а перенос выводов на реальные архитектуры был предметом веры.
- Один тулчейн на всех. GCC, LLVM, binutils, glibc, ядро Linux принимают апстрим-поддержку одной архитектуры, а не десятка несовместимых проприетарных. Специализированный чип получает отладчик, профилировщик и стандартную библиотеку бесплатно — это меняет экономику нишевых устройств сильнее, чем отсутствие роялти.
- Долгий горизонт. Промышленность, авто, медицина, космос считают жизненный цикл изделия десятилетиями. Спецификация, которую нельзя отозвать, — это управление риском, а не идеология.
- Специализация без выхода из экосистемы. Спецификация резервирует пространство кодов под пользовательские команды. Домен (обработка сигналов, криптография, ML-ускоритель) добавляет свои операции и остаётся в общей тулчейн-экосистеме. Раньше выбор был бинарным: либо лицензированное ядро без права трогать ISA, либо собственная архитектура и полностью свой стек ПО, который никто не хочет писать.
Откуда он взялся: короткая линия
История нужна не для романтики. Она объясняет, почему RISC-V выглядит как «чистый учебный RISC»: он таким и задумывался, и то, что учебная архитектура вышла в промышленность, — это следствие, а не цель.
Название — не про «пятую версию»: это пятый проект RISC в Беркли после RISC I, RISC II, SOAR и SPUR, и одновременно намёк на векторы (римская V). Ключевое решение авторов — не «сделать быстрее», а «сделать так, чтобы этим можно было пользоваться без разрешения», и оно определило всю дальнейшую конструкцию: ничего запатентованного, ничего заимствованного из чужих ISA, никаких обязательств перед унаследованным бинарным кодом.
Отсутствие легаси — это преимущество ровно один раз в жизни архитектуры, и RISC-V его потратил осознанно. У x86 в кодировке живут решения 1978 года; у ARM в AArch64 — разрыв с AArch32, который дался тяжело. RISC-V начал с чистого листа в 2010 году, зная про кэши, суперскалярность, спекуляцию и многоядерность всё, что к этому моменту знала индустрия. Дальше мы увидим, где именно это знание отпечаталось в спецификации.
Базовый набор и расширения
Главная структурная идея: маленькое замороженное ядро плюс модульные расширения. Базовый целочисленный набор RV32I содержит порядка сорока команд — арифметика, логика, сдвиги, загрузка/сохранение, переходы, пара системных. Он заморожен: ратифицированные кодировки не будут изменены никогда, это явное обязательство спецификации. Реализация, поддерживающая только базу, — законный RISC-V.
Регистров общего назначения тридцать два: x0–x31, где x0 жёстко прибит к нулю — чтение всегда даёт ноль, запись игнорируется. В варианте RV32E их шестнадцать: это уступка микроконтроллерам, где регистровый файл заметен в площади кристалла. ABI-имена (ra, sp, a0–a7, s0–s11, t0–t6) заданы отдельно от архитектуры — это соглашение о вызовах, а не свойство железа; ровно то разделение, о котором шла речь в главе про ассемблер.
Строка вида rv64gc читается так: 64-битный базовый набор + G (общепринятая аббревиатура для IMAFD с Zicsr и Zifencei) + C (сжатые команды). Именно эту строку вы отдаёте компилятору флагом -march, и именно она определяет, на каком железе получившийся бинарник запустится.
Профили: лекарство от зоопарка
Модульность порождает комбинаторный взрыв. Если каждое расширение независимо опционально, число теоретически допустимых конфигураций уходит в астрономию, а дистрибутив Linux не может собрать один бинарник для всех. Ответ — профили: ратифицированные наборы обязательных расширений, на которые ориентируются компиляторы и дистрибутивы. RVA20, RVA22, RVA23 — линейка профилей прикладного класса (машины, которые тянут полноценную ОС). Существенно, что RVA23 сделал обязательными векторное расширение и гипервизорное: до этого дистрибутивы не могли рассчитывать на векторы и собирали код по нижней планке, то есть железо умело больше, чем использовал софт.
Практический смысл: профиль — это то, что вы пишете в требованиях, а не список букв. «Нужен rv64gc» — фраза 2019 года; «нужен RVA23» — фраза, которая означает предсказуемый набор возможностей плюс обязательства по привилегированной части.
Кодирование: почему биты лежат именно так
Здесь начинается настоящая инженерия. Форматов шесть, все ровно по 32 бита в базовом наборе.
Смотреть на эту картинку надо не как на таблицу, а как на схему проводов. Три наблюдения, каждое из которых — сэкономленные транзисторы и такты:
Номера регистров всегда на одних и тех же битах. rs1 — биты 19:15, rs2 — 24:20, rd — 11:7, во всех форматах, где эти поля вообще есть. Значит, чтение регистрового файла можно начать до того, как декодер разобрался, что это за команда: адреса портов берутся напрямую с фиксированных проводов. В x86 позиция операнда зависит от префиксов, ModRM и SIB — определить, какие регистры нужны, можно только после разбора. Это одна из причин, почему широкий декодер x86 стоит заметной площади и энергии.
Знаковый бит константы всегда бит 31. Схема знакового расширения (а она нужна почти каждой команде) не зависит от формата: тянем бит 31 в старшие разряды и всё.
Биты константы в форматах B и J «перемешаны» — и это не небрежность. Кажется дикостью, что смещение перехода собирается из кусков imm[12|10:5] и imm[4:1|11]. Но посмотрите на позиции: бит 5 константы приходит с бита 25 команды и в S-, и в B-формате; биты 10:1 в J-формате лежат там же, где 10:1 в B-формате. То есть каждый бит константы приходит почти всегда с одного и того же провода, и мультиплексор, который собирает константу, получается узким. Перемешивание перенесено из железа (где оно стоило бы логики на критическом пути) в ассемблер и дизассемблер, где оно стоит десяти строк кода. Это чистый образец компромисса «сложность туда, где она дешевле».
Младший бит смещений в B и J не хранится: переходы всегда кратны двум байтам, потому что сжатое расширение C сделало допустимой двухбайтовую гранулярность команд. Один сэкономленный бит расширяет дальность перехода вдвое.
Разберём одну команду по битам
Возьмём addi x5, x6, 42. Это I-формат: opcode 0010011, funct3 000, rd = 5, rs1 = 6, imm = 42.
imm[11:0] rs1 f3 rd opcode
000000101010 00110 000 00101 0010011
Склеиваем: 0000 0010 1010 0011 0000 0010 1001 0011 = 0x02A30293. Это можно проверить руками, ассемблером и следующим декодером — все три пути обязаны сойтись.
# Минимальный декодер подмножества RV32I: разбор слова на поля.
# Сложность: O(1) по времени и O(1) по памяти на команду — только сдвиги и маски.
# Ровно поэтому декодер RISC-V дёшев в железе: фиксированная длина + фиксированные поля.
FORMAT = {0b0110011: "R", # add, sub, and, or, sll, slt, ...
0b0010011: "I", # addi, andi, slli, ...
0b0000011: "I", # lb, lh, lw, ld
0b0100011: "S", # sb, sh, sw, sd
0b1100011: "B", # beq, bne, blt, bge, ...
0b0110111: "U", # lui
0b0010111: "U", # auipc
0b1101111: "J"} # jal
def bits(w: int, hi: int, lo: int) -> int:
"""Вырезать биты [hi:lo] включительно — то же, что провод в железе."""
return (w >> lo) & ((1 << (hi - lo + 1)) - 1)
def sext(v: int, width: int) -> int:
"""Знаковое расширение значения шириной width бит."""
return v - (1 << width) if v & (1 << (width - 1)) else v
def decode(w: int) -> dict:
fmt = FORMAT.get(bits(w, 6, 0), "?")
# эти поля читаются всегда, независимо от формата — так же делает железо
out = {"fmt": fmt, "rd": bits(w, 11, 7), "rs1": bits(w, 19, 15),
"rs2": bits(w, 24, 20), "funct3": bits(w, 14, 12)}
if fmt == "I":
out["imm"] = sext(bits(w, 31, 20), 12)
elif fmt == "S":
out["imm"] = sext((bits(w, 31, 25) << 5) | bits(w, 11, 7), 12)
elif fmt == "B": # обратите внимание, как биты собираются из четырёх кусков
out["imm"] = sext((bits(w, 31, 31) << 12) | (bits(w, 7, 7) << 11)
| (bits(w, 30, 25) << 5) | (bits(w, 11, 8) << 1), 13)
elif fmt == "U":
out["imm"] = bits(w, 31, 12) << 12
elif fmt == "J":
out["imm"] = sext((bits(w, 31, 31) << 20) | (bits(w, 19, 12) << 12)
| (bits(w, 20, 20) << 11) | (bits(w, 30, 21) << 1), 21)
return out
d = decode(0x02A30293)
print(d["fmt"], "rd=x%d rs1=x%d imm=%d" % (d["rd"], d["rs1"], d["imm"]))
# I rd=x5 rs1=x6 imm=42 -> addi x5, x6, 42
Обратите внимание на строчку с комментарием «читаются всегда»: в софте это выглядит как лишняя работа, в железе — как экономия. Прочитать пять бит с фиксированной позиции стоит ноль логики; выбрать, откуда их читать, стоит мультиплексор на критическом пути выборки.
Сжатое расширение C: код-плотность без CISC-декодера
Классическая претензия к RISC — раздутый код: фиксированные 32 бита на каждую операцию, включая mv и addi sp, sp, -16. Больше байт кода — больше промахов кэша команд, а это уже прямые потери производительности (см. Подсистема памяти и Кэш и локальность).
Расширение C добавляет 16-битные формы самых частых команд. Ключевое свойство: каждая сжатая команда однозначно разворачивается ровно в одну команду базового набора. Разворот делается комбинационной схемой на входе декодера, дальше конвейер видит обычный RV32I/RV64I. То есть плотность кода куплена без переменной длины в CISC-смысле — нет команд длиной от 1 до 15 байт, есть ровно два размера, и определить размер можно по двум младшим битам.
По данным спецификации, C сокращает статический размер кода порядка 25–30% на типичном скомпилированном коде; это порядок величины, реальная цифра зависит от языка, компилятора и уровня оптимизации. Цена: сжатые формы умеют работать только с подмножеством регистров (восемь «популярных» из тридцати двух) и с ограниченными константами, и распределитель регистров в компиляторе теперь оптимизирует ещё и это.
Решения, за которые заплатили
Разберём выбор, сделанный авторами, вместе с ценой. Именно этот раздел отличает инженерное чтение спецификации от чтения пресс-релиза.
x0 = ноль
Один регистр из тридцати двух отдан под константный ноль. Взамен получены десятки псевдокоманд без единого бита нового кодирования:
| Псевдокоманда | Разворачивается в | Что происходит |
|---|---|---|
nop |
addi x0, x0, 0 |
результат выбрасывается |
mv rd, rs |
addi rd, rs, 0 |
копирование без спец-команды |
not rd, rs |
xori rd, rs, -1 |
инверсия через XOR |
neg rd, rs |
sub rd, x0, rs |
вычитание из нуля |
beqz rs, L |
beq rs, x0, L |
сравнение с нулём — частый случай |
ret |
jalr x0, ra, 0 |
адрес возврата никуда не пишем |
j L |
jal x0, L |
переход без сохранения адреса |
Цена: тридцать один регистр вместо тридцати двух и невозможность «выключить» запись результата без записи в x0. Выгода: пространство кодов не тратится на десяток частых операций, а декодер не растёт.
Флагов условий нет
В x86 есть EFLAGS, в ARM — NZCV: команда сравнения выставляет флаги, переход их читает. RISC-V сравнивает прямо в команде перехода: blt a0, a1, L — «если a0 меньше a1, перейти».
Что выиграно: нет единственного горячего архитектурного регистра, через который проходят все зависимости. Для внеочередного исполнения это существенно — переименовывать флаги и разбираться с частичными записями в них дорого, и это одна из системных сложностей x86-реализаций (подробнее — Суперскалярность). Вдобавок сравнение и переход — одна команда вместо двух, что частично компенсирует общий счёт команд.
Что проиграно: длинная арифметика подорожала. Сложения с переносом нет, и код, складывающий 128-битные числа из 64-битных половин, вынужден вычислять перенос сравнением (sltu) и добавлять его отдельно — вместо одной команды adc получается три-четыре. Для криптографии и bignum-арифметики это ощутимо; отсюда постоянные предложения расширений и появление в Zbb/Zba операций, которые часть потерь отыгрывают.
Слотов задержки нет
MIPS выставлял наружу деталь своего конвейера: команда сразу после перехода выполнялась всегда, независимо от того, состоялся переход или нет. На пятиступенчатом конвейере это экономило такт. На семиступенчатом — уже нет, а выкинуть из архитектуры уже нельзя: существующие бинарники. Это классический пример утечки микроархитектуры в контракт, и RISC-V отказался от неё сознательно.
Тот же принцип объясняет отсутствие в базе автоинкрементных режимов адресации («загрузи и подвинь указатель»): они удобны для компилятора, но превращают одну команду в две операции записи в регистровый файл, что усложняет переименование и обработку исключений. Компромисс здесь спорный — многие считают его ошибкой, потому что цикл по массиву получает лишнюю команду addi на итерацию. Хорошая иллюстрация того, что «чисто» и «оптимально» — не синонимы.
lui / auipc и позиционно-независимый код
Константа в 32-битную команду целиком не влезает. RISC-V строит большие значения парой: lui rd, imm20 кладёт двадцать старших бит, затем addi rd, rd, imm12 добавляет младшие. Более интересна вторая команда — auipc rd, imm20: «сложить старшие двадцать бит константы с текущим PC». Она даёт PC-относительную адресацию для чего угодно — данных, функций, таблиц. Позиционно-независимый код получается естественным образом, без трюков вроде «вызвать следующую команду, чтобы узнать свой адрес», которыми пользовался 32-битный x86. Плата: доступ к далёкой глобальной переменной — две команды, а линковщику приходится уметь релаксацию (схлопывать пары, когда цель оказалась близко).
Атомарные операции: два механизма, а не один
Расширение A даёт два разных инструмента, и различие между ними стоит понимать.
# Вариант 1: готовая атомарная операция «прибавить и вернуть старое»
# amoadd.w rd, rs2, (rs1) — одна команда, реализуется контроллером памяти или кэшем
amoadd.w t0, a1, (a0) # t0 = *a0; *a0 = t0 + a1 — атомарно
# Вариант 2: пара «загрузить с резервированием / записать условно»
# Универсальнее: между lr и sc можно вычислить что угодно
retry:
lr.w t0, (a0) # загрузить и поставить резервацию на адрес
addw t1, t0, a1 # произвольное вычисление над прочитанным
sc.w t2, t1, (a0) # записать, если резервация цела; t2 = 0 при успехе
bnez t2, retry # не вышло — повторить
amo* покрывает частые случаи одной командой и хорошо ложится на реализацию «операция выполняется рядом с памятью». lr/sc — универсальный примитив для compare-and-swap и любых нестандартных read-modify-write. Спецификация задаёт ограничения на тело цикла (сколько команд между lr и sc гарантированно не сорвут резервацию), чтобы прогресс был гарантирован, а не оставался на совести реализации. Тонкость, которая кусается на практике: в цикле между lr и sc нельзя делать всё подряд — обращение к памяти или системный вызов сорвут резервацию, и цикл станет вечным.
Модель памяти: RVWMO
RISC-V определяет слабую модель памяти (RVWMO): порядок обращений к памяти, видимый другим ядрам, не обязан совпадать с программным, если не расставлены барьеры fence. Это ближе к ARM, чем к x86, где действует существенно более строгая модель TSO. Слабая модель даёт реализации свободу переупорядочивать и буферизовать; плата — весь многопоточный код обязан быть корректным относительно барьеров, а не относительно «оно и так работало на моём ноутбуке».
Отдельно существует расширение Ztso — обещание более строгого порядка. Оно нужно прежде всего тем, кто транслирует x86-бинарники: без аппаратной поддержки TSO транслятор вынужден рассыпать барьеры повсюду и терять на этом заметную часть производительности. Подробный разбор моделей памяти и барьеров — глава Многоядерность.
Векторы: длина неизвестна на этапе компиляции
Это, пожалуй, самое интересное архитектурное отличие от x86. Классический SIMD (SSE, AVX, AVX-512) фиксирует ширину регистра в самой системе команд: ширина стала частью контракта, и переход на более широкие регистры требует новых команд, нового кода и раздачи разных бинарников под разное железо.
Векторное расширение RVV устроено иначе: программа не знает VLEN. Цикл выглядит так:
# Векторное сложение массивов float: c[i] = a[i] + b[i], всего n элементов
# a0 = указатель c, a1 = a, a2 = b, a3 = n
loop:
vsetvli t0, a3, e32, m1, ta, ma # «сколько 32-битных элементов возьмёшь?» -> t0
vle32.v v0, (a1) # загрузить t0 элементов
vle32.v v1, (a2)
vfadd.vv v2, v0, v1 # сложить поэлементно
vse32.v v2, (a0) # сохранить
slli t1, t0, 2 # t0 * 4 байта
add a1, a1, t1 # продвинуть указатели
add a2, a2, t1
add a0, a0, t1
sub a3, a3, t0 # осталось элементов
bnez a3, loop
vsetvli спрашивает у железа, сколько элементов оно готово обработать за раз, и железо отвечает своим числом. Один и тот же бинарник работает на реализации со 128-битными векторами и на реализации с 512-битными, используя всю ширину. Хвост массива обрабатывается тем же кодом — последняя итерация просто получит меньшее значение, никаких отдельных эпилогов. Ту же идею независимо реализовала ARM в SVE.
Цена: сложнее реализация (нужно поддерживать динамическую длину, состояние в CSR vtype и vl становится частью контекста и должно сохраняться при переключении задач), сложнее автовекторизация в компиляторе, и оптимизировать под конкретную ширину вручную труднее. VLEN у нынешних прикладных реализаций (поколение середины 2020-х) — обычно 128–512 бит; это параметр реализации, не архитектуры, и опираться на него в коде нельзя. Подробно про векторные модели — SIMD и векторные расширения.
Макрослияние вместо сложных команд
У RISC-V нет индексированной адресации со сдвигом (load rd, [rs1 + rs2*4]), нет битовых полей в базе, нет сложных операций. Официальный ответ — макрослияние (macro-op fusion): широкий декодер распознаёт частые пары команд и склеивает их в одну внутреннюю операцию. slli + add превращается в индексированный адрес, lui + addi — в загрузку константы.
Посмотрите, что здесь произошло. x86 берёт сложные команды и разбирает их на микрооперации. RISC-V берёт простые команды и собирает их в микрооперации. Обе конструкции сходятся в одной точке: внутри процессора выполняется поток простых операций, а внешняя кодировка — вопрос плотности кода и стоимости декодера. Это ровно тот вывод, к которому пришла глава RISC и CISC, и RISC-V иллюстрирует его с другой стороны.
Часть потерь закрыли расширением Zba: команды sh1add, sh2add, sh3add («сдвинуть на 1/2/3 и сложить») дают индексированную адресацию одной командой без слияния. Показательно, что понадобилось расширение: чистота базового набора имеет измеримую цену, и её пришлось доплачивать.
Привилегированная архитектура и стек прошивки
Пользовательские команды — половина контракта. Вторая половина — режимы привилегий, управляющие регистры (CSR) и то, как устроен переход между слоями.
Три режима: M (machine) — есть всегда, S (supervisor) — нужен для ОС с виртуальной памятью, U (user) — для приложений. Микроконтроллер может реализовать только M, или M+U; процессор под Linux обязан иметь все три. Расширение H добавляет виртуализацию (режимы для гостевой ОС), и тогда картинка усложняется — см. Виртуализация и контейнеры.
Важная деталь — делегирование ловушек. Регистры medeleg/mideleg позволяют машинному режиму сказать: «системные вызовы из U-режима и промахи страниц отдавай сразу ядру, меня не беспокой». Без этого каждый системный вызов делал бы два перехода вместо одного. Трансляция адресов включается записью в CSR satp, где помимо корня таблицы страниц указан режим: Sv39, Sv48, Sv57 — трёх-, четырёх- и пятиуровневые таблицы с виртуальным адресом в 39, 48 и 57 бит соответственно (детали механизма — глава Управление памятью).
Между прошивкой и ядром стоит собственный контракт — SBI (Supervisor Binary Interface). Ядро не лезет в железо таймера или контроллера прерываний напрямую: оно делает ecall в M-режим, и прошивка (обычно OpenSBI) выполняет запрос. Это позволяет одному ядру Linux работать на разных SoC без правки кода под каждый таймер.
Полная цепочка загрузки на плате обычно такая: зашитый в ПЗУ первичный загрузчик → загрузчик из SPI-флеш или SD → OpenSBI в M-режиме → U-Boot → ядро Linux в S-режиме. Описание оборудования ядро получает из device tree — того же механизма, что используется в ARM-мире. Общая механика загрузки разобрана в главе Процесс загрузки.
Чем модель отличается от ARM и x86
Таблица сравнивает свойства контракта, а не быстродействие. Скорость конкретного чипа из неё не следует ни в какую сторону.
| Свойство контракта | x86-64 | ARM AArch64 | RISC-V |
|---|---|---|---|
| Кто может реализовать | фактически двое, по историческим лицензиям | по платной лицензии от владельца | кто угодно, без разрешения |
| Длина команды | от 1 до 15 байт | 4 байта | 2 или 4 байта, задан механизм для длиннее |
| Регистров общего назначения | 16 в базе, 32 в анонсированном расширении APX | 31 плюс отдельные zero и sp | 31 плюс жёсткий ноль x0 |
| Флаги условий | есть, EFLAGS | есть, NZCV | нет, сравнение внутри перехода |
| Доступ к памяти | операнды в памяти у арифметики | только load/store | только load/store |
| Модель памяти | строгая, близка к TSO | слабая | слабая RVWMO, опционально Ztso |
| Векторы | фиксированной ширины: SSE, AVX, AVX-512 | NEON фиксированной, SVE переменной | RVV переменной длины |
| Как добавляют новое | префиксы, VEX, EVEX поверх легаси | версии архитектуры и опции | буквенные расширения плюс профили |
| Пользовательские команды | нет | ограниченно, в отдельных линейках | да, зарезервированное пространство |
| Число мнемоник | порядок тысяч, счёт зависит от методики | порядок сотен | около 40 в базе, сотни в rv64gc |
Что из этой таблицы не следует: что одна архитектура быстрее или экономичнее другой. Работа Blem, Menon и Sankaralingam «Power Struggles: Revisiting the RISC vs. CISC Debate on Contemporary ARM and x86 Architectures» (HPCA 2013) сравнивала сопоставимые по классу ARM- и x86-ядра и не нашла системного преимущества системы команд: наблюдаемая разница объяснялась микроархитектурой, техпроцессом и целевой точкой проектирования, а не набором команд. Причины физические, и мы вернёмся к ним в главах Мощность и пределы и Производство: бюджет мощности и техпроцесс двигают показатели на порядки, ISA — на проценты.
Практический вывод для инженера: фраза «мы перешли на RISC-V и стало быстрее» почти всегда означает «мы сменили поставщика ядра, техпроцесс и конфигурацию кэшей». Спрашивать надо про них.
Как это выглядит в инструментах
Попробовать можно сегодня, железо не обязательно.
# Кросс-компиляция под 64-битный RISC-V с базовым набором расширений
riscv64-linux-gnu-gcc -march=rv64gc -mabi=lp64d -O2 -c sum.c -o sum.o
# Посмотреть, что получилось: дизассемблер покажет и кодировки, и сжатые формы
riscv64-linux-gnu-objdump -d sum.o
# Запуск пользовательского бинарника без железа — эмуляция уровня приложения
qemu-riscv64 ./sum
# Полная машина: прошивка, ядро, всё целиком
qemu-system-riscv64 -machine virt -bios default -nographic -kernel Image
# Rust умеет из коробки: цель для Linux и цель для голого железа
rustup target add riscv64gc-unknown-linux-gnu
rustup target add riscv32imac-unknown-none-elf
Строка -march — это буквально список расширений, которыми компилятору разрешено пользоваться. Собрали с -march=rv64gcv — получили векторный код, который не запустится на чипе без V (программа получит исключение недопустимой команды). Собрали по нижней планке — код запустится везде и не воспользуется тем, что железо умеет. Это тот же самый компромисс, что с -march=x86-64-v3 в мире x86, только острее, потому что разброс конфигураций больше. Именно его и лечат профили.
Функция целиком, чтобы увидеть код глазами:
// sum.c — сумма массива, специально примитивная
int sum(const int *a, int n) {
int acc = 0;
for (int i = 0; i < n; i++) acc += a[i];
return acc;
}
Скалярный вариант на ассемблере RV64 выглядит примерно так (соглашение о вызовах: a0 — первый аргумент и он же результат, a1 — второй):
sum:
li a2, 0 # acc = 0
blez a1, .done # n <= 0 -> сразу выход; blez = bge x0, a1
.loop:
lw a3, 0(a0) # загрузить очередной int со знаковым расширением
addw a2, a2, a3 # 32-битное сложение: addw усекает и расширяет знак
addi a0, a0, 4 # продвинуть указатель на sizeof(int)
addi a1, a1, -1 # уменьшить счётчик
bnez a1, .loop # bnez = bne a1, x0
.done:
mv a0, a2 # результат в a0; mv = addi a0, a2, 0
ret # ret = jalr x0, ra, 0
Три вещи, видимые невооружённым глазом. Первая: addw, а не add — в RV64 отдельные команды для 32-битной арифметики, потому что int остаётся 32-битным и его надо корректно расширять знаком. Вторая: сравнение с нулём делается переходом напрямую, никаких флагов. Третья: половина строк — псевдокоманды, которые ассемблер развернёт в базовые формы; читать такой код приятнее, чем декодировать addi a0, a2, 0.
Как проверить реализацию на соответствие спецификации: существует официальный набор тестов совместимости riscv-arch-test, а эталонным симулятором служит Spike. Для самостоятельного изучения хорош godbolt.org — там есть RISC-V-компиляторы, и можно смотреть, во что превращается C или Rust, меняя -march.
Где RISC-V реально стоит сегодня
Здесь важно отделить «продаётся в огромных количествах» от «на этом работает ваш ноутбук». Это разные вопросы, и ответы на них противоположные.
Скрытые ядра: там, где счёт идёт на сотни миллионов
Самое массовое применение RISC-V вы никогда не увидите в спецификации устройства. Внутри большого SoC живёт десяток микроконтроллеров: управление питанием, инициализация памяти, обработка команд накопителя, обслуживание радиомодуля, управление датчиками. Раньше каждый производитель держал для этого собственную архитектуру или платил за лицензию. Сейчас на эти роли массово ставят RISC-V — тут не нужна совместимость с чужим софтом, весь код пишет сам производитель, а тулчейн приходит готовым.
Публично об этом рассказывали NVIDIA (замена собственных микроконтроллеров Falcon внутри GPU на ядра RISC-V) и Western Digital (ядра SweRV для контроллеров накопителей, позже открытые под именем VeeR и переданные CHIPS Alliance). Meta упоминала ядра RISC-V в своих ускорителях для инференса. Оценки суммарных отгрузок исчисляются миллиардами ядер в год, но точные числа сильно зависят от методики счёта, поэтому важен здесь порядок величины и сам факт: RISC-V уже победил в категории «процессор, о котором пользователь не знает».
Микроконтроллеры: конкурентная и живая ниша
Класс устройств, знакомый по треку Embedded: десятки мегагерц, килобайты SRAM, никакой ОС или RTOS. Здесь RISC-V стоит на равных с ARM Cortex-M. Реальные примеры: у Espressif линейка ESP32-C и ESP32-H построена на RISC-V-ядрах, GigaDevice выпускает GD32V, WCH — семейство CH32V, где младшие модели уходят в розницу за считанные центы. Порядок характеристик для этого класса (поколение начала-середины 2020-х): частоты от единиц до сотен мегагерц, память — единицы-сотни килобайт, потребление в активном режиме от долей до десятков милливатт. Конкретные цифры смотрите в даташите: разброс внутри класса больше, чем разница между архитектурами.
Почему здесь получилось: софт для микроконтроллера почти всегда собирается из исходников под конкретную плату, и вопрос «а запустится ли готовый бинарник» не стоит. Плюс работает аргумент про кастомные команды — производитель мотор-контроллера может добавить свою операцию и получить компилятор с поддержкой.
Прикладной класс: работает, но с оговорками
Одноплатники под Linux существуют и покупаются: платы на StarFive JH7110, на SpacemiT K1, многоядерные машины на Sophgo SG2042, была даже материнская плата RISC-V для модульного ноутбука Framework. Дистрибутивы подтянулись: Debian сделал riscv64 официальной архитектурой начиная с 13-го выпуска, Ubuntu и Fedora давно публикуют образы, ядро Linux поддерживает архитектуру в апстриме много лет.
Оговорки, которые нужно знать до покупки:
- Производительность на ядро у доступных сегодня плат — уровень ARM-одноплатников предыдущих поколений, а не рабочей станции. Это не свойство ISA: просто высокопроизводительные ядра (P870 у SiFive, Veyron у Ventana, Ascalon у Tenstorrent, открытый XiangShan) только выходят из стадии проектирования в кремний, а первые массовые чипы делались на простых внутрипорядковых ядрах.
- Дистрибутивы собраны по нижней планке. Пока базовым уровнем был rv64gc, готовые пакеты не могли использовать векторы, даже если чип их умеет. Переход на RVA23 как общую цель — это то, что должно закрыть разрыв, но перестройка дистрибутивов занимает годы.
- Проприетарный софт и драйверы. Всё, что распространяется бинарниками (включая драйверы GPU), нужно пересобирать, а этого не сделает никто, пока нет рынка. Классическая проблема курицы и яйца, ровно та же, через которую проходил ARM на серверах.
Где RISC-V пока нет и почему
Смартфоны, ноутбуки, серверы массового рынка. Причина не в системе команд — она покрывает эти сценарии полностью. Причина в экосистеме и в сроках: чтобы поставить процессор во флагманский телефон, нужны не только конкурентное ядро, но и полная поддержка ОС, магазин приложений с пересобранным нативным кодом, драйверы, инструменты разработчика и уверенность в поставках на годы вперёд. Показательный эпизод: Google объявляла о поддержке riscv64 в Android, а затем убрала её из общего ядра, публично объяснив, что запуск в ближайшее время не планируется. Это высказывание про готовность экосистемы, а не про качество архитектуры.
Отдельный мотив, который нельзя не упомянуть, оставаясь фактологичным: устойчивость к ограничениям на поставки. Архитектура, права на которую нельзя отозвать, привлекательна для стран и компаний, опасающихся экспортных запретов. Именно этим объясняется масштаб инвестиций в RISC-V в Китае, разработка ядер XuanTie и открытого высокопроизводительного ядра XiangShan. Инженерно это означает одно: у архитектуры появился крупный источник финансирования, не зависящий от западного рынка, и рост экосистемы от этого ускоряется.
Главный риск: фрагментация
Свобода добавлять расширения — это фича и мина одновременно.
Представим: три производителя добавили по своей команде для одной и той же операции. Компилятор должен знать про все три, дистрибутив не может использовать ни одну, а код, написанный под первого, не идёт у второго. Формально все трое соответствуют спецификации. Пользователю от этого не легче — вместо одной архитектуры получается три несовместимых.
Механизмы, которыми с этим борются:
- Профили (RVA20/RVA22/RVA23) — «если пишешь RVA23, ты получаешь вот этот набор гарантированно». Компилятор, дистрибутив и разработчик договариваются об одном имени.
- Платформенные спецификации — требования не только к ISA, но и к обнаружению устройств, прошивке, загрузке. Без этого «совместимый» чип всё равно не загрузит типовой образ ОС.
- Замороженные кодировки — то, что ратифицировано, не меняется. Программа, собранная под RV64I, будет работать всегда.
- Тесты совместимости —
riscv-arch-testкак проверка, что реализация делает то, что написано. - Дисциплина обнаружения возможностей. Программа не должна гадать: в Linux есть механизмы, через которые ядро сообщает список поддерживаемых расширений, и правильный код выбирает реализацию функции во время выполнения — ровно так же, как это делается с AVX-путями в библиотеках под x86.
Честная оценка: проблема не решена окончательно, она управляется. Так же, как в мире ARM управляется зоопарк опциональных возможностей, и как в x86 управляется зоопарк уровней микроархитектуры. Разница в том, что у RISC-V пространство вариантов шире, и дисциплина нужна строже.
Типичные заблуждения
- «RISC-V — это open source». Открыта спецификация. Ядра бывают открытыми, но большинство коммерческих реализаций — обычная закрытая IP, которую продают за деньги.
- «Открытая ISA = дешёвые чипы». Отчисления — одна строка в смете. Маски, верификация и объём тиража решают больше.
- «RISC-V эффективнее по своей природе». Энергоэффективность определяется микроархитектурой, техпроцессом и точкой проектирования. Простое ядро экономично потому, что оно простое, а не потому, что RISC-V.
- «Меньше команд — быстрее». Число мнемоник не связано со скоростью напрямую. Считать надо динамическое число выполненных операций, плотность кода (влияет на кэш команд) и IPC. Простая ISA может проиграть по числу выполненных команд и выиграть по тактовой частоте — и наоборот.
- «Добавлю свои команды и обгоню всех». Вместе с командой вы берёте на себя её поддержку в компиляторе, отладчике, симуляторе, ОС, а также верификацию и обучение своей же команды. Иногда это окупается (узкий домен, огромный тираж), чаще — нет.
- «RISC-V безопаснее, потому что проще». Спекулятивные утечки — свойство микроархитектуры, а не системы команд; внеочередное RISC-V-ядро с предсказателем переходов уязвимо ровно так же (см. Предсказание переходов). Простые внутрипорядковые ядра к таким атакам действительно устойчивее — но по причине отсутствия спекуляции, и это же лишает их производительности.
- «Открытая ISA означает, что чип можно проверить». Проверить можно спецификацию. Что реально лежит в кристалле — вопрос доверия к производителю, и он не меняется.
Мини-итог
- Открытость RISC-V — свойство спецификации, не реализации и не кристалла. Она снимает юридический шлагбаум, не снимая инженерную стоимость.
- Конструкция: маленький замороженный базовый набор (порядка сорока команд) плюс модульные расширения; профили RVA собирают расширения в предсказуемые уровни для дистрибутивов.
- Кодировка спроектирована под дешёвый декодер: фиксированные позиции номеров регистров, фиксированный знаковый бит, намеренно «перемешанные» константы ради узких мультиплексоров.
- Компромиссы честные и с обеих сторон: отсутствие флагов упрощает переименование, но удорожает длинную арифметику; отказ от слотов задержки и от автоинкремента — плата за то, чтобы микроархитектура не протекала в контракт.
- Векторное расширение независимо от длины вектора: один бинарник использует всю ширину любой реализации — модель, противоположная фиксированному SIMD.
- Привилегированная часть трёхуровневая (M/S/U), между ядром и прошивкой стоит отдельный контракт SBI — именно он делает возможным одно ядро Linux на разных SoC.
- Реальное положение дел: тотальное присутствие в скрытых контроллерах, полноценная конкуренция в микроконтроллерах, работающий, но отстающий по производительности на ядро прикладной класс, отсутствие в смартфонах и массовых серверах.
- Главный риск — фрагментация; главный инструмент против неё — профили и дисциплина обнаружения возможностей во время выполнения.
Источники
- Официальные спецификации RISC-V: https://riscv.org/technical/specifications/
- Исходники и релизы руководства по системе команд: https://github.com/riscv/riscv-isa-manual
- Профили RISC-V (RVA20/RVA22/RVA23): https://github.com/riscv/riscv-profiles
- Тесты соответствия архитектуре: https://github.com/riscv-non-isa/riscv-arch-test
- Эталонный симулятор Spike: https://github.com/riscv-software-src/riscv-isa-sim
- OpenSBI — эталонная реализация SBI: https://github.com/riscv-software-src/opensbi
- Запуск RISC-V в QEMU: https://www.qemu.org/docs/master/system/target-riscv.html
- Порт Debian на riscv64: https://wiki.debian.org/RISC-V
- Целевые платформы Rust, включая RISC-V: https://doc.rust-lang.org/rustc/platform-support.html
- David Patterson, Andrew Waterman. «The RISC-V Reader: An Open Architecture Atlas» — компактный разбор ISA от авторов: http://riscvbook.com/
- David Patterson, John Hennessy. «Computer Organization and Design, RISC-V Edition» — учебник, где вся архитектура объясняется именно на RISC-V.
- Emily Blem, Jaikrishnan Menon, Karthikeyan Sankaralingam. «Power Struggles: Revisiting the RISC vs. CISC Debate on Contemporary ARM and x86 Architectures», HPCA 2013 — работа, показывающая, что вклад системы команд в энергоэффективность мал по сравнению с микроархитектурой и техпроцессом.
- Справочник по кодировкам и расширениям в удобном виде: https://five-embeddev.com/
Что дальше
Мы разобрали контракт: какие команды существуют и как они закодированы. Но контракт ничего не говорит о том, как процессор их выполняет — а именно там живёт вся производительность. Следующая глава открывает реализацию: как процессор перестаёт делать одну команду за раз и начинает держать в работе сразу несколько, что при этом ломается и как это чинят.