Автора часто просят объяснить неприязнь к RISC-V, и обычно объяснение приходится собирать по кускам. Типичная реакция на аргументы — "ты просто не понимаешь всей гениальности архитектуры", что, конечно, вообще не является аргументом. После N-ной просьбы объяснить свою позицию было решено изложить всё в одном месте, чтобы впредь просто давать ссылку. К тому же, если кто-то захочет сформулировать связное контраргументацию, он сможет ссылаться на конкретные пункты, имея этот текст как опору. Все высказанные здесь мнения принадлежат автору и не отражают позицию работодателя, каких-либо божеств или домовладельца. Кошки автора частично согласны, частично не согласны и опубликуют свою позицию позже.
Всё для всех
Первая и самая простая для понимания проблема в том, что невозможно быть лучшим сразу во всех сценариях использования. Фанаты RISC-V хотят убедить всех, что архитектура скоро займёт место и в суперкомпьютерах, и в крошечных микроконтроллерах, и во всём, что между ними. Это невозможно, и было бы невозможно для любой ISA. Проще говоря, требования к высокопроизводительному CPU диаметрально противоположны требованиям к дешёвому микроконтроллерному ядру. Эти решения касаются не только микроархитектуры, но и (неизбежно) самой архитектуры набора команд. При всём при этом есть стопроцентная уверенность, что RISC-V займёт нишу дешёвых одноразовых микроконтроллеров. Не из-за дизайна ISA, а вопреки ему. Эта роль перейдёт от 8051 просто потому, что RISC-V станет улучшением по сравнению с ним — планка настолько низкая, что это скорее лежачий полицейский, чем препятствие.
Что нужно дешёвому микроконтроллерному ядру? Посмотрим, для чего такие ядра используются. Типичный сценарий — взаимодействие и быстрая перенастройка аппаратных блоков в составе большого чипа, например, в MP3-плеере, SD-карте или USB-флешке. Основную работу делает специализированная логика, а ядро 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. Далее нужен ещё один CSSRW, чтобы получить старое значение 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 и различных проприетарных расширений "fast IRQ" / автоматического сохранения стека — дополнительное обвинение. Базовая ISA вынуждает производителей изобретать нестандартный кремний, чтобы достичь паритета с десятилетней давности Cortex-M0. Это, в свою очередь, ещё сильнее фрагментирует "стандарт" (если его вообще можно так назвать). Забавно, что даже со сжатым расширением типичный пролог обработки IRQ оказывается больше и медленнее нулевого по размеру аппаратного пути Cortex-M0.
Теперь о сжатых инструкциях. Рассмотрим их подробно. Они спроектированы до смешного плохо. Допустим, нужно сохранить байт в память по адресу регистр плюс смещение. Какой диапазон смещений может закодировать 16-битная инструкция? От нуля до трёх. Не тридцать три, не триста три. Три! Может, для полуслова лучше? Нет... ноль или два. Что вообще происходит? Почему? По крайней мере, для слова диапазон разумный — от нуля до 124 байт, но что творится с остальными? Хуже того, инструкция для сохранения полуслова закодирована похоже на инструкцию для сохранения байта, но почему-то имеет меньше вариантов смещений? Почему? Дело в том, что один из битов, который store-byte использует для смещения, просто жёстко прошит в ноль... его можно было бы использовать, чтобы расширить диапазон хотя бы до 6, но этого не сделано! Для сравнения, Cortex-M0 спокойно позволяет использовать смещения от нуля до 31 для байтов, от нуля до 62 для полуслов и от нуля до 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-marker? Конечно, e-marker опционален. Может ли мой USB-C кабель 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 на моём железе?", разумеется. CPUID — вот как на этот вопрос отвечают на x86. Как это сделать на RISC-V? Есть хорошая новость и плохая. Есть CSR под названием misa, который может ответить на некоторые из этих вопросов (не на все, конечно, это было бы слишком разумно). Заметили проблему? Это CSR, а поддержка CSR опциональна (расширение Zicsr не обязательно). Если этого было недостаточно для удара ниже пояса, misa даже не обязан быть точным, если реализован — он может читаться как сплошные нули — сама его осмысленность полностью опциональна. Ага... единственный способ обнаружить опциональные функции сам опционален. Но это ещё не всё!
Буква "M" перед "misa" указывает, что это CSR машинного режима. Машинный режим — самый высокий уровень привилегий в RISC-V (и единственный обязательный, если кто-то ещё считает). Этот регистр недоступен для чтения из режимов с более низкими привилегиями, даже если повезло (а) работать на железе, реализующем Zicsr, (б) работать на железе, где misa не прошит намертво в нули, и (в) работать на железе, реализующем другие режимы привилегий. Это означает, что подстроить код под возможности железа из обычного пользовательского кода невозможно. Если ваше железо реализует ОПЦИОНАЛЬНЫЙ режим супервизора, он тоже не может обнаружить базовые возможности ядра. Официальная позиция — "спросите супервизора машинного режима". Проблема, очевидно, в том, что неизвестно, что это за супервизор и как его "спросить". Есть распространённая реализация под названием 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 (последнее ядро raspian для 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!
Когда такое случается, вместо понятного краша получаете... ну... что угодно. Кто может предсказать, как поведёт себя бинарник, когда сохранение регистра с плавающей точкой молча превращается в перемещение двух регистров или инструкцию перехода, или наоборот? Это не пересказ сюжета B-фильма ужасов на тему программистов. Это реально может случиться. Рассмотрим инструкцию 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, но поскольку датчики обычно работают правдоподобно и без калибровки, этого никто не заметил среди случайных падений. Отладка этого — ИМЕННО сюжет B-фильма ужасов на тему программистов, который давно пытались продать в Голливуд. Уважаемый комитет RISC-V, вы остаётесь после уроков и пишете на доске 100 раз: апгрейд процессора не должен превращать валидный бинарник в семантически другой валидный бинарник.
Мнимые исправления
Многие фанаты RISC-V утверждают, что вся эта опциональность больше не проблема, поскольку фонд RISC-V создал "профили" — списки обязательных опциональных расширений (да, перечитайте это ещё раз), которые вместе взятые позволяют заявлять о соответствии профилю. Опять же: экосистеме пришлось изобретать второй слой стандартизации, единственная цель которого — сказать, какие части первого стандарта реально можно считать существующими. Если приходится строить второй стандарт, чтобы сказать всем, какие части первого стандарта реально нужно реализовать, возможно, первый стандарт был не совсем готов. Только процесс дизайна комитетом мог произвести такую ситуацию. Чтобы избежать нацеливания на наименьший общий знаменатель 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. После публикации этой статьи планируется найти подрядчика, чтобы сделать фонарь в стиле Bat-Signal™ с логотипом Gentoo, направленный на облака над штаб-квартирой RISC-V. Gentoo, тебя призвали на битву, к которой ты, сам того не зная, готовился всю свою историю!
Как мы сюда пришли и куда двигаться дальше?
Если прочитать документ с обоснованиями/оправданиями RISC-V и отбросить все части, признанные выше откровенной ложью, преувеличениями или полным непониманием того, как работают существующие архитектуры (и мир в целом), остаётся одно предложение, которое, вероятно, объясняет всё: "В конечном счёте мы решили, что для наших целей лучше начать с чистого листа, чем модифицировать OpenRISC соответствующим образом". И это после того, как объяснили, что OpenRISC делает всё, что хотелось, за исключением задержанных переходов (delay slots), и признали, что версия без задержанных переходов уже существовала. Вот вам и объяснение: почему биты разбросаны как в лотерейном барабане, почему сотни конфликтующих расширений с конфликтующими кодировками, и почему очевидно полезных инструкций нигде не найти — просто потому, что всё это было "придумано не здесь".
Справедливости ради, другое возможное объяснение того, как мы сюда пришли, — это то, как устроены академическая среда и гранты. По своей природе грантовые заявки склонны упрощать и переоценивать реальную применимость и влияние. Учитывая, что немалая часть "исследований" 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. Это довольно близко к вышеописанному сценарию, поскольку основные вычисления будут выполняться специализированными блоками кремния, оптимизированными под множество операций MATMUL за такт. Ядро RISC-V будет настраивать DMA и перенастраивать эти большие блоки для вычисления выходов следующего слоя. Вариант такого дизайна может включать ядро RISC-V с прикреплённым очень широким векторным движком. Его можно использовать для поэлементных операций, тоже распространённых в ML-нагрузках. Опять же, это ядро не обязано быть навороченным или даже внеочередным, оно будет работать просто за счёт достаточной ширины по вектору. Его целочисленный конвейер не будет значимым фактором для производительности, а недостатки RISC-V будут восприниматься как цена, которую платят за отсутствие лицензионных отчислений за Cortex-A55. Ожидается, что RISC-V победит на этом рынке по той же причине, что и для маленьких вспомогательных ядер — реальная серьёзная производительность CPU на ядро в традиционном смысле здесь не нужна, так что достаточно хорошего ядра хватит. А традиционно, достаточно хорошие решения всегда выбираются по процессу "ORDER BY price ASC LIMIT 1;".
Вторая категория для больших вычислений — реальные десктопы и одноплатники, выполняющие интерактивные вычисления, браузинг, игры и прочую "десктопную работу". Не ожидается, что RISC-V станет серьёзным игроком на вершине этого рынка. Проще говоря, архитектура для этого не спроектирована, как показано выше. К тому же на этом рынке достаточно маржи, чтобы позволить себе лицензировать намного лучше спроектированное ядро aarch64 от ARM и получить надлежащую поддержку от намного большего корпуса программного обеспечения. Прежде чем хвататься за мегафон, чтобы кричать про "открытость", стоит отметить, что открытость спецификации RISC-V здесь вообще не при чём, поскольку открытая спецификация не материализует магическим образом хорошо спроектированное внеочередное ядро бесплатно. И если кто-то спроектирует хорошее внеочередное ядро, он не станет раздавать его бесплатно. Открытая спецификация не означает, что каждая реализация бесплатна. Средний и нижний сегменты дешёвого рынка одноплатников, вероятно, останутся смесью новых ядер RISC-V и старых ядер ARM.