Железо и архитектуры RISC-V: открытая система команд и почему это важно
0%

RISC-V: открытая система команд и почему это важно

RISC-V: открытая система команд и почему это важно

В предыдущих главах трека мы установили две вещи. Первая: система команд — это контракт между железом и программой, а не описание внутренностей процессора (Система команд). Вторая: спор RISC против CISC закончился не победой одной стороны, а тем, что обе конструкции сошлись в середине — внутри современного x86 живёт RISC-подобное ядро, а «чистые» RISC обзавелись плотными кодировками и сложными операциями (RISC и CISC).

Из этого следует неудобный вывод: если контракт и реализация разделены, то владение контрактом — это отдельная переменная, не сводимая ни к скорости, ни к энергоэффективности. Кто-то решает, что можно записать в спецификацию, кому позволено её реализовать и на каких условиях. Полвека эта переменная была скучной константой: у x86 и ARM ответ был известен и менялся редко. RISC-V меняет именно её — и уже поэтому заслуживает главы, даже если бы не принёс ни одной новой инженерной идеи. Он, впрочем, принёс: несколько решений в нём — это осознанный ответ на грабли, на которые наступили MIPS, SPARC, Alpha и Itanium.

Эта глава — не рекламный проспект. Мы не будем сравнивать «что быстрее»: скорость определяется микроархитектурой и техпроцессом, а не буквами в спецификации, и к этому мы вернёмся отдельно. Мы разберём: что технически означает «открытая» система команд, как устроен базовый набор и механизм расширений, почему поля команды лежат именно на этих битах, за какие решения пришлось заплатить и чем, как выглядят привилегированные режимы и стек прошивки, и где RISC-V физически стоит в железе на сегодня.

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

Что технически означает «открытая система команд»

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

Спецификация — текст, описывающий команды, кодировки, регистры, режимы и модель памяти. У RISC-V она опубликована свободно: её можно скачать, прочитать, реализовать в железе, продать чип и не платить ни цента отчислений и не спрашивать разрешения. Это и есть единственный смысл слова «открытая».

Реализация — конкретное ядро: конвейер, кэши, предсказатель переходов. Может быть любой: полностью закрытой коммерческой IP, купленной у поставщика, или опубликованной под свободной лицензией. Открытость спецификации о реализации не говорит ничего.

Кристалл — физическое изделие. Стоит денег в любом случае: маски, верификация, физический дизайн, тестирование (об этом — Производство). Бесплатная спецификация снижает одну статью расходов из десятка, и обычно не самую крупную.

Три контракта стека RISC-V

Три разные модели владения

Сравним не характеристики, а именно юридико-инженерные модели — это и есть предмет главы.

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» — пересекающиеся, а не совпадающие множества.

Тогда почему это вообще важно

Аргументы, которые остаются после вычёркивания маркетинга:

  1. Экспериментировать можно без юридического разрешения. Университет, стартап или исследовательская группа строит ядро, публикует его, другие воспроизводят результат. Для микроархитектуры это принципиально: раньше работы сравнивали на упрощённых учебных ISA, а перенос выводов на реальные архитектуры был предметом веры.
  2. Один тулчейн на всех. GCC, LLVM, binutils, glibc, ядро Linux принимают апстрим-поддержку одной архитектуры, а не десятка несовместимых проприетарных. Специализированный чип получает отладчик, профилировщик и стандартную библиотеку бесплатно — это меняет экономику нишевых устройств сильнее, чем отсутствие роялти.
  3. Долгий горизонт. Промышленность, авто, медицина, космос считают жизненный цикл изделия десятилетиями. Спецификация, которую нельзя отозвать, — это управление риском, а не идеология.
  4. Специализация без выхода из экосистемы. Спецификация резервирует пространство кодов под пользовательские команды. Домен (обработка сигналов, криптография, 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.

Регистров общего назначения тридцать два: x0x31, где x0 жёстко прибит к нулю — чтение всегда даёт ноль, запись игнорируется. В варианте RV32E их шестнадцать: это уступка микроконтроллерам, где регистровый файл заметен в площади кристалла. ABI-имена (ra, sp, a0a7, s0s11, t0t6) заданы отдельно от архитектуры — это соглашение о вызовах, а не свойство железа; ровно то разделение, о котором шла речь в главе про ассемблер.

Строка вида rv64gc читается так: 64-битный базовый набор + G (общепринятая аббревиатура для IMAFD с Zicsr и Zifencei) + C (сжатые команды). Именно эту строку вы отдаёте компилятору флагом -march, и именно она определяет, на каком железе получившийся бинарник запустится.

Профили: лекарство от зоопарка

Модульность порождает комбинаторный взрыв. Если каждое расширение независимо опционально, число теоретически допустимых конфигураций уходит в астрономию, а дистрибутив Linux не может собрать один бинарник для всех. Ответ — профили: ратифицированные наборы обязательных расширений, на которые ориентируются компиляторы и дистрибутивы. RVA20, RVA22, RVA23 — линейка профилей прикладного класса (машины, которые тянут полноценную ОС). Существенно, что RVA23 сделал обязательными векторное расширение и гипервизорное: до этого дистрибутивы не могли рассчитывать на векторы и собирали код по нижней планке, то есть железо умело больше, чем использовал софт.

Практический смысл: профиль — это то, что вы пишете в требованиях, а не список букв. «Нужен rv64gc» — фраза 2019 года; «нужен RVA23» — фраза, которая означает предсказуемый набор возможностей плюс обязательства по привилегированной части.

Кодирование: почему биты лежат именно так

Здесь начинается настоящая инженерия. Форматов шесть, все ровно по 32 бита в базовом наборе.

Шесть базовых форматов команд RISC-V

Смотреть на эту картинку надо не как на таблицу, а как на схему проводов. Три наблюдения, каждое из которых — сэкономленные транзисторы и такты:

Номера регистров всегда на одних и тех же битах. 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. Инженерно это означает одно: у архитектуры появился крупный источник финансирования, не зависящий от западного рынка, и рост экосистемы от этого ускоряется.

Главный риск: фрагментация

Свобода добавлять расширения — это фича и мина одновременно.

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

Механизмы, которыми с этим борются:

  1. Профили (RVA20/RVA22/RVA23) — «если пишешь RVA23, ты получаешь вот этот набор гарантированно». Компилятор, дистрибутив и разработчик договариваются об одном имени.
  2. Платформенные спецификации — требования не только к ISA, но и к обнаружению устройств, прошивке, загрузке. Без этого «совместимый» чип всё равно не загрузит типовой образ ОС.
  3. Замороженные кодировки — то, что ратифицировано, не меняется. Программа, собранная под RV64I, будет работать всегда.
  4. Тесты совместимостиriscv-arch-test как проверка, что реализация делает то, что написано.
  5. Дисциплина обнаружения возможностей. Программа не должна гадать: в 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.
  • Реальное положение дел: тотальное присутствие в скрытых контроллерах, полноценная конкуренция в микроконтроллерах, работающий, но отстающий по производительности на ядро прикладной класс, отсутствие в смартфонах и массовых серверах.
  • Главный риск — фрагментация; главный инструмент против неё — профили и дисциплина обнаружения возможностей во время выполнения.

Источники

Что дальше

Мы разобрали контракт: какие команды существуют и как они закодированы. Но контракт ничего не говорит о том, как процессор их выполняет — а именно там живёт вся производительность. Следующая глава открывает реализацию: как процессор перестаёт делать одну команду за раз и начинает держать в работе сразу несколько, что при этом ломается и как это чинят.

Конвейер: как процессор делает несколько дел одновременно

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

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

Доска запросов