Автора этого текста часто просят объяснить неприязнь к RISC-V, и обычно объяснение приходится собирать по кусочкам. В ответ нередко звучит что-то вроде «ты просто не понимаешь, насколько это гениально» — что, конечно, вообще не является аргументом. После N-ного запроса объяснить всё с нуля было решено собрать аргументы в одном месте, чтобы впредь можно было просто дать ссылку. К тому же, если кто-то захочет сформулировать связное контраргументы, у него будет чёткий и подробный список тезисов для ответа. Все мнения — личные и не отражают позицию работодателя, какого-либо божества или домовладельца. Кошки автора отчасти согласны, отчасти нет, и опубликуют собственное мнение позже.
Всё и для всех
Первая и самая простая для понимания проблема: невозможно быть лучшим сразу во всех сценариях использования. Фанаты RISC-V убеждают, что архитектура вот-вот захватит и суперкомпьютеры, и крошечные микроконтроллеры, и всё, что между ними. Это невозможно в принципе — причём было бы одинаково невозможно для любой ISA. Требования к высокопроизводительному CPU диаметрально противоположны требованиям к дешёвому микроконтроллерному ядру. Эти различия касаются не только микроархитектуры, но и самой архитектуры набора команд. При этом можно быть на 100% уверенным: RISC-V действительно займёт нишу дешёвых одноразовых микроконтроллеров. Не из-за дизайна ISA, а вопреки ему. Эту роль архитектура отберёт у 8051, просто будучи улучшением по сравнению с ним — планка настолько низкая, что это скорее лежачий полицейский, чем препятствие.
Что нужно дешёвому микроконтроллерному ядру? Посмотрим на типичные сценарии использования: взаимодействие и быстрая переконфигурация аппаратных блоков в составе большого чипа — например, в MP3-плеере, SD-карте или USB-флешке. Основную работу делает специализированный IP-блок, а ядро CPU лишь изредка трогает регистр или что-то настраивает. Важна задержка прерывания (чем меньше, тем лучше) и размер (чем меньше, тем лучше). Серьёзной математики от такого ядра обычно не ждут. В массово производимых устройствах код исполняется прямо из ROM (если прошивка не обновляется) или из RAM (если обновляется); NOR flash слишком дорога для по-настоящему массового производства. При исполнении из ROM важна плотность кода, поскольку ПЗУ занимают немало места. При исполнении из RAM плотность важна по той же причине — SRAM тоже съедает площадь кристалла. Поэтому плотность кода критична для таких сценариев. Раз серьёзной математики не ожидается, аппаратные делители (и даже умножители) можно опустить. Разделение привилегий тоже не нужно в подобных однозадачных случаях — никакой недоверенный внешний код здесь никогда не загружается. «Но постойте, — скажете вы, — вы же только что описали RV32IC (или RV32EC)!»
Действительно, довольно близко — но чтобы утверждать это, нужен RV32I_Zicsr. Без Zicsr нет спецификационно корректного способа обрабатывать прерывания, поскольку негде временно спрятать регистр, чтобы освободить место для сохранения остальных. В MIPS для этого зарезервированы два регистра ($k0 и $k1). Без них RISC-V вынужден использовать mscratch/sscratch. Без Zicsr нет и их — остаётся полагаться на странные обходные пути. И вот мы снова на территории 8051, который славится странными способами делать простые вещи. Маленькие встраиваемые ядра — не монстры с внеочередным исполнением. Если удаётся получить одну инструкцию за такт — уже повезло. С учётом этого посчитаем оптимистично, сколько тактов нужно обработчику прерывания, чтобы сохранить требуемые ABI регистры и вызвать обработчик на C. Сначала CSRRW сохранит один регистр (пусть будет t0) и даст базовый адрес, куда сложить остальные. Затем нужно сохранить ra, sp, gp, tp, t1–t6 и a0–a7. После этого ещё один CSRRW вернёт старое значение t0, которое тоже нужно сохранить. Это минимум 21 такт. На выходе арифметика похожая: один CSRRW для чтения адреса сохранённых регистров и 19 загрузок для их восстановления. Это минимум 20 тактов. Но и это ещё не всё. Поскольку всё это делается на ассемблере, нужно учесть JAL к обработчику на C и RET обратно — пусть каждый займёт по два такта. Итого каждое прерывание стоит минимум 44 такта до того, как обработчик на C вообще начнёт работать. Cortex-M0 (конкурирующее дешёвое 32-битное ядро) входит в прерывание за 15 тактов, выходит за 12, а поскольку регистры, требуемые ABI, сохраняются аппаратно, обработчик пишется на C напрямую. Итого стоимость прерывания здесь всего 27 тактов. Ого — заметно быстрее! Можно возразить, что это нечестно по отношению к RV32E: имея вдвое меньше регистров, он сохраняет и восстанавливает на 6 тактов быстрее, доводя накладные расходы до 38 тактов. Всё равно на треть больше, чем у Cortex-M0. Именно в той единственной задаче, для которой такое встраиваемое ядро и существует, RISC-V заметно хуже ведущего конкурента. Существование CLIC и различных проприетарных расширений «быстрого IRQ»/автосохранения регистров — дополнительное тому подтверждение. Базовая ISA вынуждает производителей изобретать нестандартный кремний, чтобы догнать десятилетней давности Cortex-M0. Это, в свою очередь, ещё сильнее фрагментирует «стандарт» (если его вообще можно так называть). Забавно, что даже с расширением сжатых инструкций типичный пролог обработчика прерывания оказывается больше и медленнее, чем нулевой по размеру аппаратный путь Cortex-M0.
Теперь о сжатых инструкциях подробнее. Спроектированы они на удивление плохо. Допустим, нужно сохранить байт по адресу «регистр плюс смещение». Какой диапазон смещений умещается в 16-битную инструкцию? От нуля до трёх. Не тридцать три, не триста три. Три! Может, для полуслова лучше? Нет — ноль или два. Что вообще происходит? Хотя бы для слова диапазон разумный — от 0 до 124 байт, но что творится с остальными? Хуже того: инструкция сохранения полуслова закодирована похоже на сохранение байта, но почему-то с ещё меньшим числом вариантов смещения. Почему? Один из битов, используемых сохранением байта под смещение, просто жёстко зашит в ноль — его можно было использовать, чтобы расширить диапазон хотя бы до 6, но этого не сделано! Для сравнения, Cortex-M0 спокойно даёт смещения от 0 до 31 для байтов, от 0 до 62 для полуслов и от 0 до 124 для слов — это явно покрывает куда больше реальных случаев. Что здесь произошло, доподлинно неизвестно, но оправдать это действительно трудно. Обычный контраргумент — использовать полноразмерные инструкции для больших смещений. Хорошо, но тогда пострадает плотность кода — та самая плотность, которой фанаты RISC-V так недавно хвастались, расхваливая расширение C. Но это ещё не всё. Инструкции сохранения байта и полуслова даже не входят в расширение C. Они относятся к другому расширению, Zcb, так что доступа к ним может не быть вовсе, даже если их скромный диапазон устроил бы в конкретной ситуации. К «расширениям» ещё вернёмся позже.
Что нужно серверным ядрам? Чистая пропускная способность. Здесь мы в мире внеочередных ядер, где кремний практически бесплатен, поскольку кэш всё равно во много раз больше самого ядра. Современные внеочередные ядра декодируют по восемь, а иногда десять инструкций за раз, направляя их по нескольким портам одновременно; многие современные ядра способны за один такт выполнить более одного перехода (вдумайтесь на секунду — да, именно так). Плотность кода здесь не так критична, как раньше. Она важна, но чуть больший L1i-кэш — не такая уж проблема, и площадь кристалла на таких масштабах опять же почти бесплатна. По-настоящему нужна возможность максимально легко выбирать и декодировать как можно больше инструкций за раз. При этом полезно, чтобы инструкции сообщали как можно больше о своих намерениях, позволяя объединять их с другими или разбивать максимально эффективно. На первый взгляд эти два желания противоречат друг другу, и отчасти это так. «Лёгкое декодирование» на практике означает «фиксированную длину», а «максимум информации» — «длинную инструкцию». Очевидно, фиксированные очень длинные инструкции не нужны. Где провести черту? Если длина инструкции — степень двойки, многое упрощается, включая выравнивание. Два байта — слишком коротко. Восемь байт — слишком длинно. Ответ: фиксированные 4-байтные инструкции — хороший компромисс. Этого достаточно, чтобы закодировать почти всё нужное: 3 регистра в каждой инструкции, длинные смещения для переходов. Звучит знакомо? Потому что именно так устроены aarch64 (и A32), и это доказанно работает исключительно хорошо. MIPS сделал тот же выбор по той же причине.
Здесь можно возразить, что у ARM есть Thumb, а у MIPS — microMIPS. Однако в высокопроизводительных вычислениях Thumb мёртв. Когда Apple проектировала aarch64 вместе с ARM, обширное моделирование и тестирование показали, что сжатые инструкции — чистый проигрыш и по инструкциям на ватт, и по инструкциям в секунду. MIPS наверняка тоже похоронил бы microMIPS, доживи он до нынешнего режима стоимости транзистора. Главный вывод: сжатым инструкциям нечего делать в больших ядрах — они мешают быстрому параллельному декодированию множества инструкций, замедляя поиск границ между ними. Можно возразить, что «RISC-V позволяет легко определить длину инструкции», но «легко» — не то же самое, что «мгновенно и бесплатно», что дают инструкции фиксированной длины.
Вернёмся к обсуждению чистой производительности, которую должны демонстрировать высокопроизводительные ядра. Какая операция самая распространённая в любом коде? Доступ к массиву. Именно поэтому в x86 есть режимы адресации вида [ebx + esi * 4], а в ARM — [R0, R1, LSL #2]. Без этого приходится, как идиоту, сдвигать регистр влево на два разряда, складывать с другим регистром и только потом обращаться к памяти. Три инструкции ради одного обращения к массиву. Обычная отговорка — «слияние инструкций объединит все три в одну на быстрых ядрах». Ага, конечно — если кто-то и правда такое реализует, ему стоит вручить пару премий. Ни одно известное ядро не сливает больше двух последовательных инструкций. Ни одно. Причина очевидна — комбинаторный взрыв числа возможных сочетаний, которые нужно отслеживать и обрабатывать. Итак, установив, что эта отговорка — чушь, что же сделали разработчики? Через несколько ЛЕТ после публикации спецификации предложили расширение для решения этой проблемы — Zba. Оно даёт три инструкции вида SHxADD для x равного 1, 2 или 3. Это объединяет сдвиг со сложением, сокращая доступ к массиву с трёх инструкций до двух. Всё ещё хуже, чем режим адресации «регистр + сдвинутый регистр», но теперь утверждение «ядро может их слить» звучит чуть более правдоподобно — если кто-нибудь такое ядро выпустит. Одна проблема: SHxADD всегда занимает 4 байта, а сама инструкция доступа к памяти — 2 или 4 байта, так что доступ к массиву превращается в 6 или 8 байт кода против 4 у ARM. Именно в этот момент все, кто недавно кричал о чудесной плотности расширения C и о том, как это здорово для высокопроизводительных ядер, тихо замолкают и смотрят в пол. Для полноты картины: расширение Zba ратифицировали только в 2021 году — спустя более двух лет после базовой спецификации. Им понадобилось ДВА ГОДА, чтобы осознать существование массивов!
Любопытно, что исправить это было бы легко. Сейчас 3/4 пространства кодирования отдано под сжатые инструкции (все инструкции, чьи два младших бита не равны 0b11). Использовать часть этого пространства под лучшие режимы адресации — очевидное решение, дающее более плотный код с лучшей адресацией массивов. К сожалению, это было бы слишком разумно, чтобы кто-то реально это сделал. Из всех возможных защитников здравого смысла именно Qualcomm предложила такой подход — и даже сделала прототип. Дальше дело не пошло. Когда умудряешься заставить Qualcomm звучать голосом разума — это верный признак, что где-то сильно облажались. Но вернёмся к SHxADD. Нет гарантии, что вообще получится их использовать, поскольку Zba — расширение, а значит опциональное. Уже устали слышать слово «опциональный»? Об этом дальше.
Опциональность
Что общего у RISC-V, USB-C и RCS? Формально все они — стандарты, но интересно то, что заявление о соответствии одному из них ничего не значит, оставаясь при этом технически правдой. Валиден ли кабель USB-C, разведённый только под USB 2.0? Конечно, витые пары USB 3 опциональны. Валиден ли кабель без e-маркера? Конечно, e-маркеры опциональны. Может ли USB-C кабель класса USB 3.0 не поддерживать 20 Гбит/с? Конечно, поддержка 20 Гбит/с опциональна. А поддерживать 20 Гбит/с, но не поддерживать зарядку на 100 Вт? Тоже опционально! Может ли полностью соответствующая спецификации реализация RCS в телефоне не уметь повышать текстовое сообщение до видеозвонка? Конечно — MIVC опционален. Может ли она не отправлять фото во время звонка? Конечно, опционально! Можно ли не поддерживать шифрование? Тоже опционально! Так что вообще означает «соответствие спецификации», если всё опционально? По сути это означает, что авторы спецификации слишком много времени провели в мысленной мастурбации и слишком мало — в контакте с реальным миром. Это случается двумя путями: либо академики, не подозревающие, что за пределами их кабинетов существует реальный мир, либо ситуации проектирования комитетом, где реальный мир просто никогда не получает места за столом — не успев вовремя подать заявку, чтобы председатель вынес вопрос на голосование.
Достаточно взглянуть на такую формулировку: «Расширение RISC-V B (Bit-Manipulation) — это стандартный набор улучшений набора команд, призванных повысить производительность и плотность кода за счёт эффективных битовых операций. Оно разделено на отдельные под-расширения: Zba, Zbb, Zbc и Zbs». Только процесс проектирования комитетом мог всерьёз породить такую последовательность слов.
Составляя спецификацию, каждый раз, делая что-то опциональным, вы разбиваете возможные реализации на две несовместимые группы. Сделайте это достаточно много раз — и спецификация теряет смысл. И, надо признать, разработчики RISC-V здесь знатно облажались. Опционально всё! Умножение — опционально. Деление — опционально. Поддержка пользовательского режима — опционально. Режим супервизора — опционально. CSR — опционально. Сжатые инструкции — опционально. «Дополнительные сжатые инструкции» — тоже опционально. Есть подозрение, что если бы получилось, сложение тоже сделали бы опциональным.
Базовый набор команд RISC-V на момент первой публикации включал CSR, которые, как уже упоминалось, необходимы для соответствующей спецификации обработки прерываний и поддержки разных уровней привилегий. Это разумно, здраво и не сломано. Именно поэтому это, конечно же, поспешили исправить! CSR вынесли в отдельное расширение Zicsr, и теперь базовая ISA не умеет обрабатывать прерывания или разделять привилегии. Но на самом деле всё ещё нелепее. Допустим, у вас куча опциональных возможностей. Какой первый вопрос захочет задать код? «Реализована ли функция X на моём железе?», разумеется. На x86 на этот вопрос отвечает CPUID. А как это сделать на RISC-V? Есть хорошая новость и плохая. Есть CSR под названием misa, отвечающий на часть таких вопросов (не на все, конечно — это было бы слишком разумно). Уже заметили проблему? Это CSR, а поддержка CSR опциональна (расширение Zicsr не обязательно). Если этого недостаточно: misa не обязан быть точным, даже если реализован — ему разрешено читаться как все нули, то есть сама его осмысленность тоже опциональна. Да... единственный способ обнаружить опциональные возможности сам является опциональным. Но и это не всё!
Буква «M» перед «misa» означает, что это CSR машинного режима. Машинный режим — самый привилегированный в RISC-V (и единственный необязательный к реализации, если вести счёт). Этот регистр недоступен из режимов с более низкой привилегией, даже если повезло (a) работать на железе с Zicsr, (b) работать там, где misa не зашит в нули, и (c) работать там, где реализованы другие режимы привилегий. Это значит, что настроить код под возможности железа из обычного пользовательского кода невозможно. Если железо реализует ОПЦИОНАЛЬНЫЙ режим супервизора, он тоже не может определить базовые возможности ядра. Официальная позиция — «спроси супервизора машинного режима». Проблема очевидна: неизвестно, что это за супервизор и как его «спросить». Есть распространённая реализация — OpenSBI, но, конечно, нет способа узнать, действительно ли машинный режим работает именно на ней.
Ещё один забавный кусочек опциональности — системный таймер. В спецификации RISC-V есть таймер; он, разумеется, опционален. Доступ к нему не через CSR, потому что, конечно же, это было бы слишком последовательно. Официальная отговорка — экономия пространства кодирования CSR. Видимо, опасались исчерпать... 4096 регистров?! Амбициозно, если хотите знать моё мнение — ни одна современная архитектура даже близко не подошла к этому лимиту, даже x86. Но двинемся дальше. Если регистры таймера не CSR, то где они? В памяти! Где именно? Раз таймер — базовая периферия, нужная любой ОС, и спецификация ядра CPU его описывает, логично ожидать чётко определённого адреса, на который можно положиться. Шутка! Ничто в этой спецификации не настолько разумно! Адрес «определяется реализацией» и может быть где угодно. Удачи, повеселитесь, не крашнитесь!
Расскажем ещё об одной забавной ситуации из многих возможных: векторизация исключений и прерываний. Когда происходит исключение или прерывание (при условии, что опциональный Zicsr реализован), куда прыгает CPU? В зависимости от множества опциональных и опционально поддерживаемых конфигурационных регистров, регистров делегирования и прочей избыточно усложнённой чепухи, ядро в итоге выбирает регистр вектора машинного или супервизорного режима (mtvec или stvec). Этот CSR указывает на обработчик, за исключением двух младших битов, определяющих его «режим». Что за режим? Документированы два. Прямой режим — когда младшие два бита равны 0b00, тогда все исключения и прерывания просто прыгают по адресу из старших битов. Векторизованный режим (младшие биты 0b01) призван упростить и ускорить обработку прерываний. Все исключения прыгают по адресу из старших битов регистра, а все прерывания — по этому адресу плюс 4, умноженное на номер прерывания. В чём проблема с этим на первый взгляд разумным дизайном? В том, что оба режима опциональны!!! Ни одна часть спецификации не требует даже простого прямого режима! Во время выполнения можно проверить, поддерживается ли конкретный режим, записав младшие биты и прочитав их обратно, чтобы увидеть, сохранились ли они. Но абсолютно валидна реализация ядра, поддерживающего только векторизованный режим. Или только прямой, или оба, или ни одного — если производитель ядра вместо этого изобрёл собственный отдельный режим. Это сильно усложняет написание сколько-нибудь универсального ядра ОС — буквально невозможно понять, чего ожидать. Почему прямой режим не сделали обязательным — непостижимо, но можно с уверенностью сказать, что человек, принявший такое решение, носил огромные ботинки, большой красный нос и много белого грима на лице.
На этом моменте кто-то бросится защищать эту непростительную нелепость криком одного из двух: «у других архитектур тоже есть опциональные возможности» и/или «сокрытие misa нужно для поддержки виртуализации, вы что, не читали Попека и Голдберга?». Разберём эти слабые отговорки по очереди. Что касается «других архитектур» — рассмотрим то, что было в широком использовании последние несколько десятилетий: x86, ARMv7 и Aarch64. У x86, как уже упоминалось, есть CPUID, который с готовностью сообщит, какие возможности есть у текущего ядра, причём прямо в пользовательском режиме, как и следует ожидать. У ARM есть ID_AA64PFR0_EL1 и ID_AA64ISAR0_EL1, доступные хотя бы ядру ОС (хоть и не пользовательскому пространству). Но здесь важнее другое — оно объясняет, почему дизайн ARM не фатален. И в x86, и в ARM опциональные возможности делятся на два чётких класса: (1) высокопроизводительные вычисления, обычно программируемые через интринсики или ручной ассемблер для узких циклов в особых случаях (кодирование видео, симуляции жидкости) или через libc (memcpy, memset, strlen), и (2) опциональные вещи, совместимые с NOP, которые безопасно выполняются на железе без их поддержки, просто исполняясь как NOP. Для x86 это endbr64, для aarch64 — почти весь набор инструкций PAC. Обратите внимание: ничто из нужного в совершенно обычном скомпилированном коде не является опциональным. Умножение, деление, режимы адресации всегда доступны. Это значит, что обычный компилятор C, нацеленный на эти архитектуры, не сталкивается с невозможным выбором: «компилировать под наименьший общий знаменатель, чтобы код работал на всех версиях архитектуры» или «предполагать, что умножение и разумные режимы адресации существуют, и готовиться к краху на ядре, которое решило их не реализовывать». Да, конечно, здесь скажут «просто целься в конкретное ядро, почему бы и нет». Никто не говорил, что нелепость этой ISA нельзя обойти достаточными ухищрениями. Речь о том, что такой плохой дизайн был приемлем в 1970-х, когда мы не знали лучше, и непростителен в 2000-х, когда знаем.
Теперь об отговорке про виртуализацию. Во-первых, Попек и Голдберг говорят о виртуализируемости архитектуры именно в контексте отсутствия специальной поддержки виртуализации. Действительно, перехватывая каждую инструкцию, ведущую себя по-разному в пользовательском и супервизорном режиме, можно виртуализировать любую архитектуру. Но альтернатива — просто встроить поддержку виртуализации. x86 не виртуализируем по Попеку и Голдбергу как минимум из-за инструкции POPF. И тем не менее виртуальные машины на x86-машине работают прекрасно, поскольку x86 добавил поддержку виртуализации. Пристроить её постфактум было нетривиально сложно, но это сделали. А если делать это на этапе проектирования архитектуры — тривиально. Иначе говоря, ЛЮБАЯ отсылка к Попеку и Голдбергу для оправдания решений, принятых на этапе проектирования архитектуры, — чушь. Этап проектирования архитектуры — как раз то время, чтобы сделать правильно. Попек и Голдберг даже упоминают, что перехват всего теоретически интересен для доказательства виртуализируемости, но непрактичен. НЕПРАКТИЧЕН. Так что же сделали разработчики RISC-V? Они оправдывают сокрытие misa от супервизорного и пользовательского режима фразой «а что насчёт виртуализации... что, если гипервизор хочет скрыть возможности от виртуальной машины?». Чушь собачья! И x86, и ARM прекрасно справляются с этим. И RISC-V тоже мог бы, просто позволив гипервизору лгать о содержимом misa, при этом оставив возможность его чтения для всех. Похоже, авторы действительно слышали про Попека и Голдберга, но не дочитали дальше аннотации.
Ещё одна вещь, оправдываемая невнятной отсылкой к Попеку и Голдбергу — невозможность для исполняемого кода определить, в каком режиме CPU он выполняется. Это, опять же, полная бессмыслица. x86 раскрывает это косвенно через POPF (например), а ARM даже не заставляет ухищряться, открыто предоставляя это в MSR CurrentEL. Определение текущего режима на RISC-V — целое приключение. Иногда это возможно, но не во всех случаях. Зачем это вообще нужно? Например, при написании ядра ОС, которое должно поддерживать все ядра RISC-V. Автор потратил немало времени, пытаясь заставить это работать для собственного ядра rePalm, так что можно провести по дереву решений и показать, где каждая ветка оборачивается ударом по больному месту. Во-первых, если у ядра нет Zicsr, гарантированно работаешь в машинном режиме, но узнать, что Zicsr отсутствует, можно только пробным чтением CSR — а тут две проблемы: во-первых, без Zicsr нет корректного общего способа перехватить возникшее исключение при генерации ловушки недопустимой инструкции. Во-вторых, какой CSR читать? Не забывайте, что почти все они опциональны. Может показаться заманчивым попробовать misa, но не забывайте, что вы как раз проверяете, в каком режиме находитесь. Если вы в режиме супервизора, эта проверка тоже провалится, даже если Zicsr реализован. Хорошо, можно попробовать прочитать sstatus — он доступен и супервизору, и машинному режиму. В безопасности, да? Как бы не так! Режим супервизора опционален, и если ядро его не реализует, регистра sstatus просто нет — получаете ловушку. Ладно, упростим задачу. Предположим, Zicsr существует. Можно ли тогда хотя бы отличить режим S от режима M? Нет! Заманчивым кажется такое решение: установить stvec на обработчик, который просто сдвигает sepc вперёд на 4 и возвращается (пропуская проблемную инструкцию), затем выполнить чтение misa. Если вы были в машинном режиме — чтение пройдёт успешно. Если в режиме супервизора — возникнет ловушка, и любой разумный машинный монитор передаст вам ловушку недопустимой инструкции. Тогда обработчик пропустит инструкцию, вы это заметите и заключите, что были в режиме супервизора. Победа, да? Почти... Ещё раз: режим супервизора опционален. Представьте, что вы были в машинном режиме на ядре без поддержки режима супервизора. Вы попадёте в ловушку, как только попытаетесь установить stvec, поскольку его не существует. Захочется просто перехватить и эту ловушку тоже? Для этого нужно установить mtvec, а в этом нельзя быть уверенным, поскольку, возможно, вы всё это время были в режиме супервизора. Так пересечение тотальной опциональности и полного непонимания авторами Попека и Голдберга ставит бедного разработчика в крайне неприятное положение.
Очевидные пробелы
Несмотря на наличие расширения буквально на всё, включая операции над содержимым кухонной раковины, ряд очевидно полезных инструкций почему-то отсутствует. Отсутствие режимов адресации «регистр + регистр» уже разобрано, возвращаться к этому не будем. Есть и другие очевидные «низко висящие фрукты», которые явно проигнорировали. И прежде чем возразить, что RV32I проектировался простым — всё предлагаемое ниже крайне тривиально и в основном сводится к простым проводкам на ASIC.
Первое и самое главное: проверка бита с переходом по его значению. Одна такая инструкция заменяет две (SLLI + BGEZ/BLTZ), да ещё и не требует временного регистра. Чтобы проверить, насколько часто это встречалось бы, если бы существовало, был взят случайный бинарник aarch64 (последнее ядро raspbian для raspberry pi, «vmlinuz-6.1.0-49-arm64», sha256: B3B686DE 82CC7B84 EFEB8F6B 309A4E6C 53E7461F 281D3E56 F42E4AA3 B6207075), дизассемблирован, и подсчитано число вхождений TBZ/TBNZ. Их оказалось 35 393. Для сравнения — RET встретился 70 109 раз, а значит в среднем каждая вторая функция использует или могла бы использовать инструкцию перехода по значению бита. Реализовать это в железе тривиально, а переход по биту крайне распространён при разборе протоколов или работе с битовыми полями. Для крупных внеочередных ядер переименование регистров упрощается, когда затрагивается на один регистр меньше, да и не нужно пытаться слить две инструкции, когда есть одна. Для маленьких MCU-ядер, где слияния нет вовсе, это простой выигрыш в размере кода и скорости. Почему эту очевидную вещь не сделали — неизвестно.
Следующая крупная претензия — операции с битовыми полями: извлечение и вставка битового поля. Они крайне полезны при работе с сетевыми пакетами и аппаратными регистрами. Извлечение битового поля можно эмулировать двумя инструкциями — SLLI + SRLI/SRAI, в зависимости от требуемой знаковости. Вставка битового поля требует куда больше работы для эмуляции: создать инвертированную маску, выполнить AND с регистром назначения, сдвинуть исходный регистр на нужную позицию, выполнить OR с регистром назначения. В зависимости от битов, это легко 3-6 инструкций. BFC (очистка битового поля) — более простой частный случай, тоже весьма полезный. Реализуется за 2-3 инструкции. В железе же это просто провода — никакой сложной логики, ничего! Не нужно быть таким изощрённым, как это сделано в aarch64, хотя разработчикам RISC-V стоило бы поучиться у aarch64 хотя бы паре трюков, включая изящную обработку битовых полей. Проведён тот же подсчёт на том же образе ядра. Обнаружено 6284 инструкции вставки битового поля и 8881 инструкция извлечения — то есть в среднем две из каждых девяти функций используют эти операции. В довершение всего, для RISC-V есть расширение битовых операций — Zbs. По нему сразу видно, что авторы — академики. Оно чистое, простое, легко объяснимое, элегантное — и абсолютно бесполезное. Кому вообще нужно извлекать всего один бит? Серьёзно, какая упущенная возможность.
Нелепое кодирование
| Откуда берутся иммедиаты в 32-битных инструкциях | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Формат | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| J-type | 20 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 11 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | ||||||||||||
| U-type | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | ||||||||||||
| I-type | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | ||||||||||||||||||||
| S-type | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | ||||||||||||||||||||
| B-type | 12 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 11 | ||||||||||||||||||||
RISC-V — первая известная архитектура, которая разбрасывает иммедиаты по инструкции случайным образом без какой-либо очевидной причины. Из-за этого эмулировать её крайне неприятно, поскольку собирание значения иммедиата обратно занимает очень много времени по сравнению с такими архитектурами, как MIPS или ARM. Первая просто использует биты 0..15 для иммедиатов, вторая — более изощрённое кодирование, но вариантов там всего несколько. Оправдание разработчиков RISC-V было такое: «одни и те же биты иммедиата всегда берутся из одних и тех же битов инструкции» — оправдание настолько идиотское, что физически больно даже притворяться, будто в него веришь. Так рассуждает разработчик ПО, никогда не писавший на Verilog и думающий, что это как-то упрощает жизнь. На самом деле, как ни крути, мультиплексор для иммедиатов всё равно нужен, поскольку они разной длины и с разным числом завершающих нулей в зависимости от инструкции. А этому мультиплексору совершенно всё равно, какие именно биты инструкции подключены к его входам — это просто провода. Но даже если само обоснование идиотское, проверим утверждение по существу — выполнили ли разработчики то, что заявляли. Одни и те же биты иммедиата действительно всегда берутся из одного и того же места? Посмотрим. Для инструкций типа I бит 1 иммедиата берётся из бита 21 инструкции, бит 11 иммедиата — из бита 31. Для инструкций типа S те же биты иммедиата берутся из битов 8 и 31 соответственно. Для инструкций типа B — из битов 8 и 7. А для инструкций типа J — из битов 21 и 20. Как видите, они действительно всегда берутся из одного и того же места, как и обещано — если просто игнорировать смысл слов «одного» и «того же».
Но это лишь верхушка айсберга безумия кодирования! Для инструкций типа J иммедиат разбросан в следующем порядке: 20 10 9 8 7 6 5 4 3 2 1 11 19 18 17 16 15 14 13 12. Какое возможное объяснение придумать этому безумию, кроме того, что разработчики перепутали выкрики лототрона с правильным порядком битов для иммедиатов? И всё же это ничто по сравнению с тем беспорядком, что учинили в наборе сжатых инструкций...
| Откуда берутся иммедиаты в 16-битных инструкциях | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Инструкция | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| C.LWSP | 5 | 4 | 3 | 2 | 7 | 6 | ||||||||||
| C.SWSP | 5 | 4 | 3 | 2 | 7 | 6 | ||||||||||
| C.LW C.SW | 5 | 4 | 3 | 2 | 6 | |||||||||||
| C.J C.JAL | 11 | 4 | 9 | 8 | 10 | 6 | 7 | 3 | 2 | 1 | 5 | |||||
| C.BEQZ C.BNEZ | 8 | 4 | 3 | 7 | 8 | 2 | 1 | 5 | ||||||||
| C.LUI | 17 | 16 | 15 | 14 | 13 | 12 | ||||||||||
| C.ADDI16SP | 9 | 4 | 6 | 8 | 7 | 5 | ||||||||||
| C.ADDI4SPN | 5 | 4 | 9 | 8 | 7 | 6 | 2 | 3 | ||||||||
| C.LBU C.SB | 0 | 1 | ||||||||||||||
| C.LH C.LHU C.SH | 1 | |||||||||||||||
| C.JT C.JALT | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | ||||||||
| все остальные | 5 | 4 | 3 | 2 | 1 | 0 | ||||||||||
Здесь не меньше 9 (девяти!) форматов инструкций, и это не считая Zcb, добавляющего ещё 8! Но и это не всё! В зависимости от инструкции один и тот же формат (например CI) кодирует иммедиаты по-разному в тех же битовых позициях. С учётом этого получается почти по формату кодирования на каждую инструкцию! Это не было неизбежно! Thumb и microMIPS оба показывают примеры, как не облажаться настолько сильно, и всё же, несмотря на доступные примеры правильного подхода, разработчики RISC-V предложили нам держать их коллективное пиво, приправленное ЛСД, и взялись за дело максимально пессимальным образом. Начнём, конечно, с иммедиатов в C, поскольку «они всегда берутся из одного и того же места слова инструкции», ну вы поняли ;)
Далее при обсуждении кодирования иммедиатов будет использоваться «x» для обозначения места, где иммедиат разбит на части и там находится что-то другое. C.LWSP (загружающая слово со стека) кодирует иммедиат в порядке: 5 x x x x x 4 3 2 7 6, а C.SWSP, её родственница для сохранения на стек, кодирует иммедиат как 5 4 3 2 7 6. C.LW (загружающая слово из памяти по адресу в регистре) кодирует иммедиат как: 5 4 3 x x x 2 6, естественно. Её родственница C.SW использует то же кодирование, показывая, что команда разработчиков упустила возможность перемешать здесь ещё немного битов. Если нужно загрузить байт из памяти, используется C.LB, чьё кодирование иммедиата вообще ничего общего с предыдущим не имеет. Порядок битов: 0 1. Для загрузки полуслова нужна C.LHU, чей порядок битов — просто: 1. Потому что, конечно же, так и должно быть! Даже не будем касаться CM.PUSH и CM.POP, поскольку их кодирование настолько сложное, что спецификация посвящает целую главу объяснению, как их декодировать. Поистине, признак простого и интуитивного кодирования.
Заявляя об использовании всего доступного пространства кодирования в 16-битных инструкциях, некоторые «низко висящие фрукты» настолько низки, что требуют предупреждающих табличек по технике безопасности! Простой пример: логические сдвиги содержат 6 бит иммедиата. Аргумент — это нужно для 64-битных инструкций, но уже существует немало кодировок в наборе сжатых инструкций, доступных только для RV64. То есть RV64C и RV32C уже несовместимы. Раз так, почему все инструкции сдвига в RV32C несут лишний нулевой бит? 32-битный регистр нельзя сдвинуть больше чем на 32 бита. Спецификация требует, чтобы этот бит был нулевым, и тем не менее никакое кодирование не использует пространство, открывающееся при единичном значении этого бита. Самообман — это убеждать себя, что эффективно используешь пространство кодирования, при этом сохраняя возможность сдвигать 32-битные регистры на 61 бит, друзья мои. RV32E ещё более вопиющий пример — многие его сжатые инструкции несут лишний бит для кодирования регистров, которых там попросту не существует. Поскольку RV32E несовместим по ABI с RV32I и они никогда не разделяют код, дать RV32E более полезные кодировки, используя эти лишние биты (которые кодировали бы регистры x16..x31), было бы отличной идеей. Наверное, поэтому этого и не сделали.
Продолжим... порядок битов C.J мог бы, вероятно, пройти статистический тест NIST для генераторов случайных чисел: 11 4 9 8 10 6 7 3 2 1 5. Что вообще?! C.BEQZ/C.BNEZ тоже переходы, только условные, поэтому, конечно же, их кодирование почти не имеет ничего общего с предыдущим: 8 4 3 x x x 7 6 2 1 5. При кодировании иммедиата для C.LI порядок битов: 5 x x x x x 4 3 2 1 0. Почти выглядит разумно, но не отчаивайтесь, впереди ещё веселее. Допустим, нужно скорректировать указатель стека. Для этого есть C.ADDI16SP с иммедиатом, закодированным как: 9 x x x x 4 6 8 7 5. А если нужен адрес переменной на стеке, есть C.ADDI4SPN с иммедиатом: 5 4 9 8 7 6 2 3. Всё очень логично, разумно и понятно, как и обещано.
Представьте, что вы CPU (или эмулятор), пытающийся понять, как декодировать инструкцию. Если вы разумный CPU, скорее всего это выглядит так: посмотреть на 1-3 бита, чтобы определить формат инструкции, затем посмотреть на 2-5 бит, чтобы понять саму инструкцию — и готово, можно выполнять. Если вы RISC-V, всё немного... сложнее. Сначала смотрите на два младших бита, чтобы понять, 2 или 4 байта занимает инструкция; для 2-байтных инструкций смотрите на три старших бита, чтобы определить, что это за инструкция. Пока всё хорошо. Но затем... замечаете, что у этой инструкции есть иммедиат. Нужно собрать головоломку заново. И тут вспоминаете, что некоторые операции «регистр-регистр» кодируются в формате иммедиата, с магическим значением иммедиата, указывающим, что это именно такие операции. Например, C.NOT закодирована именно так, магическое значение — 0b111101. C.ZEXT.W (если железо реализует нужную мешанину расширений, чтобы она вообще была) использует магический иммедиат 0b111100. Каким-то образом microMIPS и Thumb справились без этого безумия. Как? Древние секреты, безвозвратно утраченные, судя по всему, ещё до рождения авторов RISC-V.
Если этого мало, стоит отметить, что в наборе сжатых инструкций есть конфликтующие кодирования в зависимости от того, какие расширения реализованы. Некоторые комбинации расширений просто невозможны (например, Zcmp и D). Это более серьёзный провал, чем все предыдущие, поскольку он не косметический и не связан с эффективностью. Несмотря на ДЕСЯТИЛЕТИЯ накопления груза обратной совместимости, даже x86 умудрился избежать очевидной ловушки — одна и та же последовательность байт означает разное для разных реализаций одной и той же архитектуры. Повторим: архитектура с печально известным ужасным кодированием умудрилась сохранить семантическую стабильность почти за 50 лет, а архитектура с чистого листа, спроектированная людьми, у которых были десятилетия чужого опыта, судя по всему, не смогла сделать это даже за пять лет. Нормально, когда у архитектуры есть нереализованные кодировки, которые становятся реализованными инструкциями в поздних версиях. Оправданно иметь реализованные инструкции, которые становятся нереализованными позже. Однако когда кодировки меняют смысл (или, того хуже, изначально имеют разный смысл) в разных реализациях одной и той же архитектуры — это безумие! Кажется, целая глава об этом была в DSM-5!
Когда такое происходит, вместо понятного краха получаешь... ну... что угодно. Кто предскажет, как поведёт себя бинарник, когда сохранение числа с плавающей точкой незаметно превращается в перемещение пары регистров или инструкцию перехода, или наоборот? Это не сюжет второсортного хоррора на тему программистов. Такое реально может случиться. Рассмотрим инструкцию 0xA002. В зависимости от ядра она может сохранить регистр двойной точности 0 по смещению стека 0 (C.FSDSP f0, 0(sp)) или прыгнуть куда-то (CM.JT 0). Но хотя бы случайный переход, вероятно, вызовет крах достаточно быстро, чтобы заметить проблему. Рассмотрим 0xAC66. В зависимости от ядра она может сохранить регистр двойной точности на стек (C.FSDSP f25, 0x18(sp)) или переместить a0 в s0, а a1 в s1 (CM.MVA01S s0, s1). Никаких изменений потока управления. Просто два повреждённых регистра и значение, не сохранённое на стек. Кто-то попытается возразить, что такая путаница невозможна, поскольку все знают (или должны знать), какое у них ядро и что оно делает. Эти «кто-то» явно никогда не сталкивались с реальным миром. Часто получаешь бинарные блобы от вендоров и просто линкуешь их в код микроконтроллера. Производители MEMS-сенсоров печально известны поставкой своих «супер проприетарных» алгоритмов калибровки именно так. Представим, что такой бинарник слинкован в код продукта и поставлен на MCU с расширением Zcmp. Всё шло гладко, пока не перешли на более крупный MCU ради лучшей поддержки чисел с плавающей точкой для более серьёзной математики, нужной новому продукту. Новый MCU поддерживает числа с плавающей точкой двойной точности. Внезапно бинарник начинает случайно падать в случайных местах и вести себя некорректно. Никаких ловушек «неопределённая инструкция», никакой сразу очевидной причины. Оказывается, новое ядро поддерживает расширение D и потому не может поддерживать Zcmp. Но из-за переиспользованных кодировок это не обнаружилось обычным способом — ловушкой недопустимой инструкции. Вместо этого недели ушли на отладку случайных крашей при повреждении случайных регистров. Дизассемблеры не помогали — они корректно дизассемблировали инструкции одним из двух валидных способов. Отладчики тоже мало помогали — они показывали, что некоторые инструкции якобы ничего не делают, а через некоторое время несколько слов на стеке повреждались. Обработчики прерываний переписывались несколько раз, тратя массу времени инженеров, чтобы убедиться в корректном сохранении и восстановлении контекста. Инженеры поддержки вендора MCU провели дни на месте, помогая электронщикам исключить помехи в линиях питания как причину случайного повреждения регистров и стека. Кроме того, калибровка сенсора была сломана всё время с момента апгрейда MCU, но, поскольку сенсоры обычно работают приемлемо и без калибровки, этого никто не заметил на фоне случайных крашей. Отладка всего этого — как раз сюжет для второсортного хоррора на тему программистов, который уже некоторое время предлагается Голливуду. Дорогой комитет RISC-V, вы остаётесь после уроков и пишете на доске 100 раз: апгрейд процессора не должен превращать валидный бинарник в семантически другой валидный бинарник.
Мнимые исправления
Многие фанаты RISC-V утверждают, что проблема тотальной опциональности решена, поскольку RISC-V Foundation создал «профили» — списки обязательных опциональных расширений (да, перечитайте это ещё раз), совместная реализация которых позволяет заявлять о соответствии профилю. Ещё раз: экосистеме пришлось изобрести второй уровень стандартизации, единственная цель которого — сказать, какие части первого стандарта реально можно считать существующими. Правда, если приходится строить второй стандарт с единственной целью сообщить всем, какие части первого стандарта реально нужно реализовать, возможно, первый стандарт был не совсем готов. Только процесс проектирования комитетом мог породить такую ситуацию. Чтобы избежать необходимости целиться в наименьший общий знаменатель RISC-V, или позора сборки тысяч чуть разных билдов каждого пакета под все возможные комбинации расширений, большинство производителей, достаточно наивных, чтобы надеяться на реальное появление настольных RISC-V систем, планируют требовать RVA23. Ubuntu, Red Hat и Android — все в этой группе. Забавная история: из всех доступных на момент публикации и рекомендованных одноплатных компьютеров на RISC-V примерно ни один реально не соответствует RVA23 — ни VisionFive 2 от StarFive, ни Banana Pi BPI-F3, ни Lichee Pi 4A, ни Orange Pi RV2, ни даже HiFive Premier P550. Ни один из них никогда не запустит Ubuntu LTS или будущие сборки AOSP. Забавно, что в некоторых онлайн-обсуждениях уже говорят о ядрах «почти RVA23». Опять же: профиль, придуманный для решения фрагментации, несовместим с большей частью железа, продающегося сейчас как «RISC-V», тем самым фрагментируя мир RISC-V на «почти-RVA23» и «пост-RVA23», причём большинство текущих плат — «почти». Фанаты назовут эту систему профилей победой, но на деле это прямое обвинение против преступления бесконечной опциональности и людей, считавших это когда-то хорошей идеей. Большим настольным ядрам не нужно, чтобы инструкция деления, адресация массивов и базовый SIMD были опциональными. И никому не следовало тратить ДЕСЯТЬ ЛЕТ на осознание этого. К тому же немало вендоров, чьи чипы не дотянут до планки RVA23, теперь, вероятно, в затруднительном положении, поскольку Android и Linux оставляют их позади, сколь бы «почти» они ни были. Это им наука за то, что купились на всё это, полагаю. Урок про FOMO и раннее вхождение — иногда оно окупается лишь «почти». Забавная деталь: есть один дистрибутив Linux, идеально подходящий для этого безумного ландшафта RISC-V — Gentoo, поскольку его философия всегда заключалась в сборке каждого пакета из исходников под конкретные особенности и оптимизации хост-CPU. То есть идеальная (читай: единственная реально применимая) модель дистрибуции ПО для RISC-V — это, судя по всему, Gentoo. После публикации этой статьи придётся найти подрядчика для изготовления Бэт-сигнала™ с логотипом Gentoo, чтобы направить его на облака над штаб-квартирой RISC-V. Gentoo, тебя призвали на битву, к которой ты, сам того не зная, готовился всё своё существование!
Как мы сюда пришли и куда двигаться дальше?
Если прочитать документ с обоснованием/оправданием RISC-V и отбросить всё, что в предыдущих разделах было признано откровенной ложью, преувеличением или полным непониманием того, как устроены существующие архитектуры (и мир в целом), остаётся одна фраза, вероятно, объясняющая всё: «В конечном счёте мы решили, что для наших целей лучше начать с чистого листа, а не модифицировать соответственно OpenRISC». Это после того, как авторы объясняют, что OpenRISC делает всё желаемое, за исключением наличия слотов задержки, и тут же признают, что версия без слотов задержки уже существовала. Вот вам и ответ: почему биты разбросаны как в лототроне, почему существуют сотни конфликтующих расширений с конфликтующими кодировками, и почему очевидно полезные инструкции нигде не найти — просто потому что всё это было «придумано не здесь».
Справедливости ради, ещё одно возможное объяснение того, как мы сюда пришли — то, как устроена академическая среда и гранты. По своей природе грантовые заявки склонны упрощать и переоценивать практическую применимость и влияние. Учитывая, что немалая часть «исследований» RISC-V велась в рамках разных грантов, вполне вероятно, что стимулы академического мира повлияли на результат. Поскольку эту гипотезу нельзя проверить имеющейся информацией, на этом и остановимся.
Означает ли это, что RISC-V обречён?
Ничто из сказанного не означает, что RISC-V обречён. Как уже отмечалось, вполне ожидаемо, что он займёт нишу, сейчас занятую 8051, — типичное дешёвое встраиваемое ядро, устанавливаемое там, где нужно немного логики, или где мощному ускорителю или DMA-движку требуется лёгкий присмотр. Во многом как с ядром Linux — цена подходящая. ARM требует лицензионные отчисления, тогда как спецификация RISC-V бесплатна, и (это ключевой момент) существуют ядра, которые можно лицензировать бесплатно. Для сценариев, где производительность не важна, а цена важна, RISC-V победит просто за счёт стоимости. «Достаточно хорошо» — низкая планка в этом случае, и RISC-V как раз нужного роста, чтобы её перепрыгнуть. И этот рынок не блестящий, но он важен и нуждается в свежей крови. Однако важно, чтобы RISC-V случайно не решил, что его выбрали за качество. Ему нужно усвоить, что выбрали его за дешевизну. Это не оскорбление дешёвых маленьких ядер — было написано немало ассемблера для всевозможных паршивых ядер с паршиво спроектированными ISA. RISC-V действительно шаг вперёд по сравнению с PIC и 8051, каким бы скромным ни был этот комплимент.
Для больших вычислений, на взгляд автора, есть две отдельные категории. Ускорители ML, вероятно, тоже в итоге получат ядра RISC-V в качестве обвязки. Это довольно близко к описанному выше сценарию, поскольку основные вычисления будут выполняться в специализированных блоках кремния, оптимизированных под множество операций умножения матриц за такт. Ядро RISC-V будет настраивать DMA и перенастраивать эти крупные блоки для вычисления выходов следующего слоя. Вариант такого дизайна может включать ядро RISC-V с прикрученным к нему очень широким векторным движком. Это может использоваться для поэлементных операций, тоже распространённых в ML-нагрузках. И снова, это ядро не обязано быть навороченным или даже внеочередным — оно будет работать просто за счёт достаточной ширины векторной части. Его целочисленный конвейер не будет существенным фактором производительности, а недостатки RISC-V будут восприниматься как цена за отказ от лицензирования Cortex-A55. Ожидается, что RISC-V победит и на этом рынке по той же причине, что и для маленьких «нянек»-ядер — реальная серьёзная производительность на ядро в традиционном смысле здесь не нужна, так что подойдёт достаточно хорошее ядро. А традиционно достаточно хорошие решения всегда выбираются по принципу «ORDER BY price ASC LIMIT 1;».
Вторая категория больших вычислений — настоящие десктопы и одноплатники для интерактивных вычислений, браузинга, игр и прочей «десктопной работы». Не стоит ожидать, что RISC-V станет серьёзным игроком в верхнем сегменте этого рынка. Архитектура попросту не спроектирована под это, как показано выше. Кроме того, у этого рынка есть маржа, позволяющая лицензировать гораздо лучше спроектированное ядро aarch64 у ARM и получить полноценную поддержку от куда более обширного корпуса ПО. Прежде чем хвататься за мегафон и кричать про «открытость» — заметим, что открытость спецификации RISC-V здесь вообще не при чём, поскольку открытая спецификация не материализует магическим образом хорошо спроектированное внеочередное ядро бесплатно. И если кто-то спроектирует хорошее внеочередное ядро, отдавать его бесплатно он не станет. Открытая спецификация не означает, что каждая реализация бесплатна. Средний и нижний сегменты рынка дешёвых одноплатников, вероятно, останутся смесью новых ядер RISC-V и старых ядер ARM.