[Перевод] 8 мифов о микропроцессорах RISC-V — ответ критикам

RISC-V — это архитектура набора команд (ISA) для микропроцессоров, которую либо любят, либо ненавидят. Наблюдается даже некоторое соперничество между лагерем ARM и RISC-V.

Возможно, не без причины. RISC-V и ARM представляют собой совершенно разные философии о том, как должен быть спроектирован RISC-чип. RISC-V разделяет долгосрочную позицию с акцентом на простоту и не загоняет себя в угол из-за краткосрочной выгоды с долгосрочными проблемами. RISC-V по-настоящему следует философии RISC: чтобы все было действительно простым не только с минимальным набором команд, но и с простыми инструкциями.

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

Как следствие, в ARM множество инструкций, выполняющих довольно много работы. В ARM есть инструкции для сложных режимов адресации, а также инструкции условного выполнения (только 32-битный ARM). Такие инструкции исполняются только при выполнении условия, при этом не вызывая ветвлений.

И у ARM, и у RISC-V есть как достоинства так и недостатки. Но в критике лучше оперировать фактами, а не заблуждениями. Относительно RISC-V существует довольно много заблуждений, и я хочу их развеять. Постараюсь охватить наиболее популярные из тех, с которыми я столкнулся.

Миф 1: RISC-V «раздувает» размер программ

Инструкция RISC-V в среднем делает меньше работы, чем ARM. В ARM есть инструкция LDR для загрузки данных из памяти в регистр. Она создана для выполнения типичного кода C/C++, например:

// Код C/C++
int a = xs[i];

Например, мы хотим загрузить данные из массива xs по индексу i. Можно перевести код в ARM, где регистр x1 содержит стартовый адрес массива xs, а регистр x2 — индекс i. Нам надо посчитать сдвиг в байтах от начального адреса x1. Для массива 32-bit целых это соответствует умножению индекса i на 4 для получения сдвига в байтах. Для ARM то же самое можно сделать двойным сдвигом регистра x2 влево.

x1 ← mem[x1 + x2<<2]

mem соответствует основной памяти. Мы используем x1, x2 и сдвиги 2, 4 или 8 для получения исходного адреса. Целиком инструкция будет выглядеть так:

; 64-битный код ARM
LDR x1, [x1, x2, lsl #2] ; x1 ← mem[x1 + x2<<2]

В RISC-V эквивалент потребует целых три инструкции (# отмечает комментарии):

# Код RISC-V
SLLI x2, x2, 2   # x2 ← x2 << 2
ADD  x1, x1, x2  # x1 ← x1 + x2
LW   x1, x1, 0   # x1 ← mem[x1 + 0]

Выглядит это как огромное преимущество ARM: код плотнее в 3 раза. Вы получаете меньшее использование кэша и большую производительность конвейера команд.

Сжатые инструкции спешат на помощь!

Однако RISC-V поддерживает сжатые инструкции добавлением всего 400 логических вентилей (AND, OR, NAND) к чипу. Таким образом, две инструкции (из наиболее частотных) могут поместиться в 32-битное слово. Круче всего, что сжатие не добавляет задержек — это не zip-декомпрессия. Декомпрессия выполняется в процессе обычного декодирования инструкций, поэтому она «мгновенная».

# Код RISC-V
C.SLLI x2, 2     # x2 ← x2 << 2
C.ADD  x1, x2    # x1 ← x1 + x2
C.LW   x1, x1, 0 # x1 ← mem[x1 + 0]

Сжатые инструкции следуют закону Парето:

20% инструкций используются 80% времени.

Не воспринимайте это буквально. Создатели RISC-V тщательно отобрали инструкции и сделали их частью сжатого набора команд.

Таким образом RISC-V легко превосходит ARM в плотности кода.


На самом деле современные ARM-чипы могут работать в двух режимах: либо в 32-битном режиме (AArch32) для обратной совместимости, либо в 64-битном режиме (AArch64). В 32-битном режиме чипы ARM поддерживают сжатые инструкции Thumb.

Уточнение: Не путайте 64-битный режим с длиной инструкции. Инструкции в 64-битном режиме по-прежнему имеют ширину 32 бита. Смысл 64-разрядного ARM заключается в возможности работать с 64-разрядными регистрами общего назначения. Для 64-разрядного режима ARM полностью переработал набор команд, поэтому нам надо четко понимать, о каком наборе команд мы говорим.

Такое различие менее важно для RISC-V, где 32-битный набор команд (RV32I) и 64-битный набор команд (RV64I) почти идентичны. Это связано с тем, что дизайнеры RISC-V думали о 32-разрядных, 64-разрядных и даже 128-разрядных архитектурах при разработке RISC-V ISA.


Вернемся к сжатым инструкциям RISC-V. Хотя меньшее потребление памяти инструкциями хорошо для кэша, это не решает всех проблем. У нас все еще больше инструкций для декодирования, выполнения и записи результатов. Однако это можно решить с помощью макро-оперативного слияния (macro-op fusion, МОС).

Уменьшаем количество инструкций макро-оп слиянием (МОС)

МОС превращает несколько инструкций в одну. Создатели RISC-V отмечают паттерны кода, которые компиляторы должны выдавать, чтобы помочь проектировщикам чипов RISC-V добиться МОС. Одно из требований состоит в том, чтобы у операций был один и тот же регистр назначения (место, куда сохраняется результат). В нашем примере обе инструкции ADD и LW сохраняют результат в регистр x1, мы можем соединить их в одну операцию в декодере инструкций.

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

Я уже писал более подробные статьи на эту тему:

Замечание о МОС

МОС — это не бесплатный обед. Вам нужно добавить больше транзисторов в декодеры для поддержки МОС. Вам также придется работать с разработчиками компиляторов, чтобы они генерировали шаблоны кода, которые создают шаблоны инструкций, которые можно слить. Разработчики RISC-V как раз занимаются этим в данный момент. Иногда МОС просто не сработает, например, из-за границ линии кэша (cache-line). Однако эти проблемы актуальны и для x86 с их инструкциями произвольной длины.

Миф 2: Инструкции переменной длины усложняют параллельное декодирование инструкций

В мире x86-инструкция в принципе может быть бесконечной длины, хотя для практических целей она ограничена 15 байтами. Это усложняет разработку суперскалярных процессоров, которые параллельно декодируют несколько команд. Почему? Потому что, когда вы получили, скажем, 32 байта кода, вы не знаете, где начинается каждая отдельная инструкция. Решение этой проблемы требует использования сложных методов, которые часто требуют больше циклов для декодирования.

Подробнее: Decoding x86: From P6 to Core 2 — Part 1

Есть мнение, что методы, используемые в Intel и AMD, основаны на грубом методе проб и ошибок, когда просто делаются многочисленные предположения о том, где инструкции начинаются и заканчиваются.

Однако дополнительная сложность сжатых инструкций для RISC-V тривиальна. Выбранные инструкции всегда будут выровнены по 16 бит. Это значит, что каждый 16-битный блок — это либо начало 16/32-битной инструкции, либо конец 32-битной инструкции.

Этот простой факт можно использовать для разработки различных способов параллельного декодирования инструкций RISC-V. Я уже не дизайнер чипов, но даже я могу придумать схему для достижения этого. Например, мог бы связать декодеры с каждым 16-битным блоком, как показано на диаграмме ниже.

Затем каждый декодер получит свой первый вход из связанного 16-битного блока. Таким образом, первая часть инструкции для декодера D2 будет взята из блока команд B2. А вторая часть — из B3, если у нас 32-битная инструкция.

При декодировании декодер определяет, 16-битная или 32-битная инструкция перед ним, и решает, использовать ли второй входной поток из следующего блока Важное замечание: каждый декодер решает, использовать ли второй 16-битный блок данных инструкций, независимо от других декодеров. Значит, это можно делать параллельно.

Для сравнения, я понятия не имею, как бы я декодировал x86-инструкции, которые могут быть длиной от 1 до 15 байт. Выглядит это как значительно более сложная задачка.

В любом случае, создатели RISC-V говорят, что это не сложная проблема. То, что я описал, — попытка объяснить, почему это несложно, с точки зрения не профессионала. Только не зацикливайтесь на предложенном решении — очевидно, что ему не хватает деталей. Например, вам точно понадобится какая-то логика для переключения мультиплексоров.

Миф 3: Отсутствие условного исполнения было ошибкой

В 32-битном коде ARM (ARMv7 и более ранние) есть условные инструкции (conditional instructions). Берем обычную инструкцию вроде LDR (загрузка в регистр) или ADD и отслеживаем условие выполнения  EQ для равенства или NE для неравенства — получаем соответствующие условные инструкции LDREQ and ADDNE.

; 32-битный ARM код
CMP   r6, #42
LDREQ r3, #33 ; r3 ← 33, if r6 = 42
LDRNE r3, #12 ; r3 ← 12, if r6 ≠ 42

CMP   r3, #8
SUBS  r3, r6, #42
ADDEQ r3, #33     ; r3 ← r3 + 33, if r6 - 42 = 0
ADDNE r3, #12

Инструкция CMP устанавливает один из флагов, проверяемых при выполнении условных инструкций. Инструкции исполняются только в том случае, если условие выполнено. Если непонятно, предлагаю изучить дополнительные материалы:

  • ARM, x86 and RISC-V Microprocessors Compared — разбор уникальных особенностей каждого процессора, включая условное выполнение.

  • Conditional Execution — разбор условного выполнения на 32-разрядной архитектуре ARM (AArch32) Azeria Labs (отличные учебники ARM).

В прикрепленной статье я также объясняю, почему ребята из RISC-V отказались от условного исполнения. Она значительно затрудняет реализацию Out-of-Order Execution (OoOE), а это очень важно для создания высокопроизводительных чипов. На самом деле современные 64-разрядные процессоры ARM (ARMv8 и выше) не имеют условного исполнения по этой же причине. В них есть только инструкции по условному отбору (CSEL, CSINC), но они выполняются безусловно.

Подробнее: Conditional select instructions in ARM AArch64

С другой стороны, в лагере ARM отсутствие условных инструкций убивает производительность даже на простых микропроцессорах без OoOE (многие 32-разрядные ARM). Предполагается, что в процессоре с малым количеством транзисторов не реализовать достаточно сложный предсказатель ветвлений, способный избежать потери производительности из-за ошибок ветвлений (miss-prediction).

Но у ребят из RISC-V снова есть решение. Если вы посмотрите на ядра серии SiFive 7, они фактически реализовали МОС, чтобы справиться с этим. Короткие ветви из одной инструкции могут быть объединены в одну ARM-подобную условную инструкцию. Рассмотрим код:

# Код RISC-V
BEQ x2, x3, done  # перейти к done, если x2 == x3
ADD x4, x5, x6    # x4 ← x5 + x6
done:
SUB x1, x3, x2

Поскольку переход к done — это одна инструкция, чип серии SiFive 7 может распознать этот шаблон и объединить в одну инструкцию. В псевдокоде это будет:

# Код RISC-V
# если x2! = x3 затем x4 ← x5 + x6
ADDNE x2, x3, x4, x5, x6
SUB x1, x3, x2

Эта функция называется short-forward-branch-оптимизацией. Подчеркну, что инструкции ADDNE нет — это просто способ пояснить происходящее. Преимущество такого МОС в том, что мы избавляемся от ветвления. И значит, мы избавляемся от затрат на ошибки предсказания ветвления (вызывающее сброс конвейера инструкций). Вот что пишут SiFive в патентной заявке:

Например, условное ветвление через одну или несколько инструкций может быть объединено. В некоторых реализациях условное ветвление объединяется со следующей инструкцией. Таким образом, комбинация выполняется как одна инструкция без ветвления.

<...>

МОС может выполняться как инструкция без ветвления, что позволяет избежать сброса конвейера, а также избежать замусоривания состояний предсказателя ветвлений. Последовательность макроопераций для объединения может включать в себя множество инструкций, следующих за условной инструкцией (control-flow instruction).

В лагере ARM могут возразить, что полагаться на МОС плохо, потому что это добавляет сложности и транзисторов. Однако ARM обычно использует МОС для объединения CMP с инструкциями ветвления типа BEQ или BNE. В RISC-V условные переходы выполняются в одну инструкцию, и только около 15% кода — это инструкции ветвления.

Таким образом, RISC-V имеет большее преимущество. ARM может поспорить с этим преимуществом, используя МОС. Но раз macro-op fusion — честная игра для ARM, то RISC-V тоже может пользоваться этим инструментом.

Комментарий переводчика

Тут мой мозг немного вскипел, но я постарался перевести понятно. Если вы уверены, что у меня не получилось, пишите в комментариях — попробуем разжевать это вместе.

Миф 4: RISC-V использует устаревшую векторную обработку вместо современных SIMD-инструкций

Некоторые люди в лагере ARM хотят создать впечатление, что дизайнеры RISC-V застряли в прошлом и не в курсе последних достижений микропроцессорной архитектуры. Дизайнеры RISC-V решили использовать векторную обработку вместо SIMD (Single Instruction Multiple Data). Ранее это было популярно в старых суперкомпьютерах Cray. Позднее SIMD были добавлены в x86-процессоры для мультимедийных приложений.

Может показаться, что SIMD — это более новая технология, но это не так. SIMD впервые появились на компьютере Lincoln TX-2, используемом для реализации первого графического интерфейса под названием Sketchpad, созданного Иваном Сазерлендом.

Векторная обработка в суперкомпьютере Cray была гораздо более продвинутым способом обработки множества элементов данных. Причина, по которой эти машины впали в немилость, заключалась в том, что суперкомпьютеры производились в штуках, и обычные ПК просто превзошли их в количестве.

SIMD были добавлены в x86-процессоры довольно хаотично, без планирования, для получения небольшого повышения скорости в работе с мультимедиа.

SIMD проще, чем набор команд для настоящей векторной обработки. Однако с расширением возможностей SIMD и увеличениями их длины мы получили нечто более сложное, чем векторная обработка. Нечто неряшливое и негибкое.

Факт, что Cray использовал векторную обработку в 1980-х годах, не означает, что векторная обработка — устаревшая технология. Это все равно, что сказать, будто колесо — это устаревшая технология, потому что оно давненько появилось.

Векторная обработка стала актуальной, потому что от нее выигрывает машинное обучение, продвинутая графика и обработка изображений. Это области, в которых нужна производительность. И ребята из RISC-V не единственные, кто это понял. ARM добавила свои собственные инструкции векторной обработки — SVE2. Если бы они были устаревшими или ненужными, пожалуй, их не добавляли бы в процессоры.

Миф 5: Векторная обработка не нужна, просто используйте видеокарты

Еще один аргумент против векторной обработки, который мне встретился, состоит в том, что для более длинных векторов вообще не нужен CPU. Просто оставьте их современной видеокарте.

Однако в таком подходе есть несколько проблем:

  1. Не всегда RISC-V-код будет выполняться на высокопроизводительных рабочих станциях с мощными видеокартами.

  2. Передача данных между памятью CPU и графического процессора вызывает задержки.

  3. Видеокарты на самом деле не спроектированы для векторной обработки общего назначения, даже если они используются для этого и преуспевают в этом.

Появляются небольшие микроконтроллеры с ИИ-ускорителями для выполнения на устройствах задач машинного обучения. В таких маленьких и дешевых системах вы не будете подключать большую видеокарту Nvidia для обработки векторных данных.

Передача данных для обработки на видеокарту и обратно добавляет накладные расходы и необходимость в планировании. Если у вас нет достаточного количества данных для отправки, то эффективнее обрабатывать данные локально на CPU.

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

Недаром многие стартапы стремятся бросить вызов Nvidia на рынке ИИ, создавая специализированные карты-ускорители ИИ. Они видят возможность конкурировать в создании оборудования, предназначенного специально для ИИ, а не графических вычислений.

Например, Esperanto Technologies с чипом SOC-1. Создали решение на основе процессорных ядер RISC-V, конкурирующее с GPU Nvidia A100. Карта A100 была специально разработана для ускорения вычислений машинного обучения. При этом в A100 чуть более 100 ядер (многопроцессорных SM).

Esperanto Technologies реализовали 1092 ядра в чипе, который потребляет всего 20 Вт мощности. A100, для сравнения, потребляет 250-300 Вт.

Это позволяет собрать шесть чипов SOC-1 на одной плате и уложиться всего в 120 Вт. Обычный стоечный сервер потребляет около 250 Вт.

Таким образом, можно сделать плату с более чем 6000 ядрами RISC-V и уместить в обычный сервер для выполнения задач искусственного интеллекта.

Все это возможно благодаря маленьким и простым ядрам RISC-V с векторной обработкой. Esperanto использует эту стратегию вместо создания многопроцессорных ядер, как на видеокарте Nvidia.

Другими словами, идея «просто используйте видеокарту для векторной обработки» не работает. Хорошо продуманные RISC-V-ядра с векторной обработкой превзойдут видеокарту в векторной обработке и будут потреблять меньше энергии.

Откуда появился озвученный в подзаголовке миф? От людей, представляющих CPU только в виде больших жирных ядер в стиле x86. Добавление векторной обработки к нескольким большим x86 ядрам не имеет смысла. Но кто сказал, что вам нужно создавать большие жирные ядра? С RISC-V можно создавать ядра любого типа. SOC-1 имеет как жирные out-of-order суперскалярные ядра, так и крошечные in-order ядра c векторной обработкой.

Миф 6: Современный ISA должен справляться с целочисленным переполнением

Часто критикуется факт, что RISC-V не вызывает аппаратного исключения (hardware exception) и не устанавливает никаких флагов в случае переполнения при исполнении целочисленных арифметических инструкций. Кажется, почти в любой дискуссии о RISC-V в качестве аргумента используется тезис, что дизайнеры RISC-V застряли в 1980-х годах (тогда никто не проверял на целочисленные переполнения).

Тема сложная, так что начнем с простого. Большинство компиляторов для популярных языков не вызывают переполнения по умолчанию. Примеры:

  • C/C++ и Objective-C

  • RUST (не для релиза)

  • GO

  • Java/Kotlin

  • C# (используйте checked блок)

Некоторые из них не делают этого даже в режиме отладки по умолчанию. В Java для исключений переполнения надо вызвать addExact и subtractExact. В C# надо написать код в контексте checked:

try {
    checked {
        int y = 1000000000;
        short x = (short)y;
    }
}
catch (OverflowException ex) {
    MessageBox.Show("Overflow");
}

В Go надо использовать библиотеку overflow с функциями overflow.Add, overflow.Sub или варианты, вызывающие «панику» (аналог исключения), такие как overflow.Addp и overflow.Subp.

Единственный популярный компилируемый язык, вызывающий исключение при целочисленном переполнении, — это Swift. И если погуглить, обнаружится много недовольных этим людей. Так что утверждение о серьезности недостатка этой фичи в RISC-V кажется странным.

Многие языки с динамической типизацией, например Python, при переполнении изменяют размер целых чисел на более крупные типы (с 16 бит на 32, потом на 64 и так далее — прим. переводчика). Но эти языки настолько медленные, что дополнительные инструкции, необходимые на RISC-V, не имеют значения.

Миф 7: Обработка целочисленного переполнения раздута

Обычно при обсуждении целочисленного переполнения утверждается, что RISC-V требует в четыре раза больше кода, чем другие архитектуры. Однако это худший случай. Большинство случаев можно обработать, просто добавив инструкцию ветвления.

Проверка переполнения при сложении беззнаковых целых чисел:

add  t0, t1, t2        # t0 ← t1 + t2
bltu t0, t1, overflow  # перейти к overflow, если t0 < t1

Знаковые целые числа, когда известен знак одного аргумента:

addi t0, t1, 42        # t0 ← t1 + 42
blt  t0, t1, overflow  # перейти к overflow, если t0 < t1

Общий случай с двумя операндами, для которых знак неизвестен:

add  t0, t1, t2        # t0 ← t1 + t2
slti t3, t2, 0         # t3 ← t2 < 0
slt  t4, t0, t1        # t4 ← t0 < t1
bne  t3, t4, overflow  # перейти к overflow, если t3 ≠ t4
Примечание переводчика

Тут не осилил проверку кода в уме. Если нашли ошибку или что-то непонятно, напишите мне.

Миф 8: Целочисленное переполнение дешево в реализации

Получение и хранение бита переполнения недорого только в сферическом вакууме. Это все равно что сказать, будто функция мутации в программировании недорогая с точки зрения памяти или циклов процессора. Но сейчас не об этом.

Один из аргументов, за который топят адвокаты чистых функций (non-mutating) в программировании, в том, что их легче выполнять параллельно, чем мутирующие c общим состоянием. То же самое относится и к out-of-order суперскалярным процессорам. Суперскалярные процессоры выполняют множество команд параллельно. Выполнение инструкций параллельно проще, если инструкции не имеют общего состояния.

Регистры статуса вводят общее состояние и, следовательно, зависимости между инструкциями. Вот почему RISC-V был разработан так, чтобы не иметь никаких регистров статуса. Инструкции ветвления в RISC-V, например, сравнивают два регистра напрямую, а не читают регистр статуса.

Для поддержки регистров статуса в суперскалярном процессоре, где инструкции выполняются не по порядку, вам нужен более сложный учет. Речь идет не просто о добавлении пары триггеров для хранения бита.

Итог

Большая часть критики RISC-V основана на непонимании технологий и отсутствии общей картины. То, как RISC-V позволяет создавать гетерогенную вычислительную среду с ядрами, оптимизированными для различных типов вычислений, работающих вместе, непривычно для людей.

Так или иначе, это не новая идея. Playstation 3 имел аналогичную архитектуру под названием Cell с ядром общего назначения Power Processor Element (PPE) и векторными процессорами специального назначения Synergistic Processing Elements (SPE).

Однако PPE и SPE имели разные наборы команд, что осложняло работу с архитектурой Cell. У современных систем RISC-V преимущество в одинаковом базовом наборе команд и регистров для ядер всех типов.

В блоге Selectel  мы не первый раз пишем про эту архитектуру. Вам может быть интересно:

Что означает RISC и CISC?

Оценка RISC-ов: когда ожидать серверы на ARM в дата-центрах

Esperanto: производительный 1000-ядерный RISC-V процессор для систем машинного обучения

 

Источник

Читайте также