Железо и архитектуры RISC и CISC: спор, который закончился не победой, а смешением
0%

RISC и CISC: спор, который закончился не победой, а смешением

RISC и CISC: спор, который закончился не победой, а смешением

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

Эта глава — про то, что за спором стояло на самом деле. Не про то, «кто победил»: никто не победил, границы размылись, и обе стороны взяли друг у друга ровно то, что оказалось работоспособным. Интереснее другое — какие инженерные ограничения породили каждую позицию, какие из них исчезли, какие остались и что из этого до сих пор влияет на ваш код.

Базовую механику считаем известной: цикл выборки-декодирования-исполнения, регистры и АЛУ разобраны во вводном треке (Как работает процессор, От кода до исполнения), регистры, стек и соглашения о вызовах на практике — в главе про ассемблер (Основы ассемблера). Здесь — следующий уровень: почему одни наборы выглядят как сетка одинаковых 32-битных слов, другие — как поток байтов переменной длины, и во что это обходится при постройке конвейера (Конвейер) и внеочередного ядра (Суперскалярность и внеочередное исполнение).

Откуда взялся спор: дорогая память и слабые компиляторы

Чтобы понять CISC, надо мысленно вернуться в мир, где память стоит дороже логики. В середине 1960-х оперативная память на ферритовых сердечниках стоила порядка нескольких долларов за байт — это не преувеличение, а порядок величины того времени. Программа в сотню килобайт была дорогим объектом сама по себе, и каждый сэкономленный байт кода имел прямую денежную стоимость. Историю этих физических и экономических ограничений мы разбирали отдельно (От ламп до кристалла).

Из этой экономики следовали три решения, которые сегодня выглядят странно, а тогда были очевидны.

Плотное кодирование. Если байты дороги, команда должна быть короткой ровно настолько, насколько нужно: частые операции получают однобайтовые опкоды, редкие — длинные, длина становится переменной. У x86 она и сегодня от 1 до 15 байт.

Микропрограммирование. Идею предложил Морис Уилкс ещё в 1951 году: вместо отдельной жёстко зашитой схемы управления на каждую команду внутри процессора кладут маленькую быструю память с микропрограммами, а команду разворачивает в последовательность микроопераций крошечный внутренний секвенсор. Добавить сложную команду — дописать микропрограмму, а не переразвести схему. Именно микрокод сделал возможной IBM System/360 (1964) — первую линейку, где одна система команд реализована в машинах разного класса и цены: дешёвая модель исполняла те же команды медленно, длинными микропрограммами на узком тракте данных, дорогая — быстро. Совместимость на уровне ISA как коммерческий инструмент родилась именно здесь.

Ставка на человека, а не на компилятор. В 1970-х генерация кода была слабой, много критичного кода писали на ассемблере руками. Отсюда идея семантического разрыва: пусть команды будут ближе к тому, что человек хочет выразить, — не «сдвинь, сложи, загрузи», а «вызови процедуру, сохранив вот эти регистры», «вставь элемент в очередь», «вычисли многочлен».

VAX-11 как вершина этой логики

DEC VAX-11 (1977) довёл идею до предела: порядка трёх сотен команд, около полутора десятков режимов адресации, ортогональность — почти любой операнд любой команды может быть задан любым режимом адресации, включая косвенный с автоинкрементом. Длина команды — от одного байта до нескольких десятков. В наборе есть POLY для вычисления многочлена, INSQUE/REMQUE для работы с двусвязной очередью, CALLS с маской сохраняемых регистров.

С точки зрения человека, пишущего на ассемблере, это прекрасно. С точки зрения того, кто строит быстрый конвейер, — катастрофа. Emer и Clark в 1984 году опубликовали измерения реальной загрузки VAX-11/780 и получили среднее около десяти тактов на команду: компактная и выразительная система команд исполнялась примерно вдесятеро медленнее, чем могла бы машина, делающая по команде за такт.

Что именно измерили в конце 1970-х

Революция RISC началась не с манифеста, а с измерений. Независимо смотрели три группы: IBM 801 (Джон Кокк, вторая половина 1970-х) — проект, выросший из контроллера телефонной станции в исследование «какой минимум команд нужен хорошему компилятору»; Berkeley RISC (Дэвид Паттерсон, 1980–1981) — кристалл RISC-I порядка сорока с небольшим тысяч транзисторов, спроектированный студентами за пару семестров; Stanford MIPS (Джон Хеннесси, 1981–1984) — ещё меньше транзисторов и ещё радикальнее ставка на компилятор. Общий вывод состоял из трёх наблюдений.

  1. Компиляторы не пользуются богатством ISA. Реальные программы состоят из загрузок, сохранений, сложений, сравнений и переходов. Чтобы применить экзотическую команду, компилятор должен распознать в исходнике ровно тот паттерн, под который она заточена, а такого совпадения почти не бывает.
  2. Сложная команда часто медленнее эквивалентной последовательности простых. Микропрограмма обязана обрабатывать общий случай — все режимы адресации, все проверки. Компилятор знает частный случай и выпускает три простые команды, которые исполняются быстрее одной сложной.
  3. Микрокод занимает площадь, которую можно потратить лучше. В машине конца 1970-х микрокодовое ПЗУ и секвенсор могли занимать заметную долю кристалла — ту же площадь можно отдать под регистры или кэш.

Программную статью «The Case for the Reduced Instruction Set Computer» Паттерсон и Дитцель опубликовали в 1980 году (ACM SIGARCH Computer Architecture News). Ответ сторонников сложных наборов — «Instruction Sets and Beyond: Computers, Complexity, and Controversy» (Колвелл и соавторы, IEEE Computer, 1985) — стоит прочитать именно потому, что он не проиграл: многие его возражения оказались верными.

Канон RISC: это было пять решений, а не одно

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

Решение RISC-канон CISC-канон Что это даёт конвейеру
Длина команды фиксированная переменная адрес следующей команды известен до декодирования
Доступ к памяти только load и store почти любая операция может работать с памятью у команды не больше одного обращения к памяти, стадии не дублируются
Режимы адресации 1–3 простых десяток и больше вычисление адреса всегда одинаковой сложности
Регистры 16–32 общего назначения 8 и меньше, часто специализированные меньше обращений к памяти, проще переименование
Управление жёстко зашитая логика микропрограмма нет внутреннего секвенсора на критическом пути

Ключевая строка — вторая. Архитектура load/store означает, что арифметика работает только с регистрами, а память трогают отдельные команды. Это не про удобство: стадия конвейера, которая ходит в память, ровно одна, и её место фиксировано. У VAX команда ADD могла обратиться к памяти дважды за операндами и третий раз за результатом, причём с косвенной адресацией — внутри одной команды могло случиться несколько промахов кэша и несколько страничных отказов подряд, а восстанавливаться после исключения в середине такой команды приходилось честно.

RISC принёс и идеи, о которых потом пожалели, — полезное напоминание, что контракт ISA нельзя отозвать. Слот задержки перехода: раз конвейер всё равно успевает выбрать следующую команду до разрешения перехода, объявим, что она всегда исполняется, а компилятор заполнит слот полезным. В ISA протекла глубина конкретного конвейера; когда конвейеры стали глубже, а исполнение внеочередным, слот превратился в обязательство, которое надо честно эмулировать. Регистровые окна (Berkeley RISC, затем SPARC): вызов процедуры сдвигает окно видимых регистров и сохранять ничего не надо — красиво, пока окна не кончились, а дальше ловушки на выгрузку и загрузку и подорожавшее переключение контекста в ОС. RISC-V сознательно не имеет ни того ни другого.

Кодирование команд: почему длина — не косметика

Самое живучее реальное различие между семействами — не количество команд, а формат.

Фиксированная и переменная длина команды

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

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

Практическое следствие для ширины машины: наращивать число декодеров у фиксированного формата дешевле. Порядок величины для больших ядер середины 2010-х — середины 2020-х: примерно от четырёх до восьми команд за такт в декодировании, и конкретное число сильно зависит от поколения, производителя и от того, идёт ли поток из кэша микроопераций или из декодеров. Таблица «у кого сколько» без указания конкретного ядра и года здесь бессмысленна.

Обратная сторона медали — плотность кода. Фиксированные 4 байта означают, что nop, ret и «сложить два регистра» стоят столько же, сколько сложная операция. На больших ядрах это неважно: кэш команд первого уровня порядка десятков килобайт. На микроконтроллере, где всей флеш-памяти сотни килобайт, это важно настолько, что ради плотности придумывают сжатые кодировки: Thumb и Thumb-2 у ARM, расширение C у RISC-V (16-битные формы частых команд). Экономию обычно заявляют в десятках процентов размера кода относительно чисто 32-битного кодирования, но точная цифра зависит от кода и компилятора. Ограничения микроконтроллеров разобраны в треке про встраиваемые системы (Основы железа).

Как x86 перестал быть CISC внутри

Переломный момент наступил в 1995 году, когда Pentium Pro (и почти одновременно AMD K5) начали делать вещь, которая сегодня кажется очевидной: декодер переводит команду x86 во внутренние операции, а исполняет процессор уже их. Наружу торчит контракт x86, внутри работает машина, устроенная по всем принципам, за которые ратовали сторонники RISC: регистровые операции, переименование, внеочередной планировщик, отставка по порядку.

Фронтенд различается, бэкенд одинаковый

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

Микрослияние и макрослияние: движение в обратную сторону

Пока x86 разбирал сложные команды на простые, оптимизация пошла и в обратную сторону — склеивать соседние операции, чтобы фронтенд и буфер переупорядочения работали с меньшим числом сущностей. При микрослиянии команда вида «сложить регистр со значением из памяти» едет до планировщика как одна сущность и распадается на обращение к памяти и арифметику только там, где это необходимо: плотная CISC-команда оказывается не недостатком, а способом сэкономить пропускную способность фронтенда. При макрослиянии пара «сравнение + условный переход» превращается в одну внутреннюю операцию — ровно тот приём, который в RISC-V встроен прямо в ISA, где сравнение и переход изначально одна команда beq/bne/blt.

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

Одна и та же функция на трёх системах команд

Возьмём предельно скучный цикл и посмотрим, во что он превращается. Код на C:

// Сумма массива целых. Специально без векторизации:
// нас интересует форма команд, а не скорость.
int sum(const int *a, int n) {
    int s = 0;
    for (int i = 0; i < n; i++) {
        s += a[i];
    }
    return s;
}

x86-64, схематично, в стиле вывода компилятора при умеренной оптимизации:

sum:
        xor     eax, eax              ; s = 0
        test    esi, esi
        jle     .Ldone                ; n <= 0 — выходим
        mov     ecx, esi              ; счётчик
        xor     edx, edx              ; i = 0
.Lloop:
        add     eax, [rdi+rdx*4]      ; загрузка и сложение — ОДНА команда,
                                      ; адрес считается прямо в ней: база + индекс * масштаб
        add     rdx, 1
        cmp     rdx, rcx
        jne     .Lloop                ; сравнение и переход — две команды,
                                      ; но фронтенд склеит их в одну операцию
.Ldone:
        ret

AArch64:

sum:
        mov     w9, wzr               // s = 0
        cmp     w1, #0
        b.le    .Ldone
        mov     x10, #0               // i = 0
        mov     w12, w1               // граница цикла
.Lloop:
        ldr     w11, [x0, x10, lsl #2] // загрузка: база + индекс со сдвигом — тоже режим адресации
        add     w9, w9, w11            // арифметика отдельно: это load/store архитектура
        add     x10, x10, #1
        cmp     w10, w12               // флаги NZCV пишет только команда с суффиксом S или cmp
        b.ne    .Lloop
.Ldone:
        mov     w0, w9
        ret

RV64, вариант с указателем вместо индекса:

sum:
        li      a5, 0                 # s = 0
        ble     a1, zero, .Ldone      # сравнение с нулём И переход — одна команда
        slli    a2, a1, 2             # n * 4 байта
        add     a2, a0, a2            # адрес конца массива
.Lloop:
        lw      a4, 0(a0)             # единственный режим адресации: регистр + смещение
        addi    a0, a0, 4             # шаг указателя приходится делать вручную
        add     a5, a5, a4
        bne     a0, a2, .Lloop        # сравнение двух регистров и переход — одна команда
.Ldone:
        mv      a0, a5
        ret

Что видно из трёх фрагментов, если смотреть не на синтаксис, а на устройство. x86 экономит команды на адресации: [rdi+rdx*4] выполняет умножение на 4 и сложение внутри команды и совмещает загрузку с арифметикой — четыре команды в теле цикла. AArch64 экономит меньше, но тоже имеет масштабируемый индексный режим: пять команд, зато каждая ровно 4 байта и декодируется тривиально. RISC-V экономит на переходах: регистра флагов нет, сравнение и переход — одна команда; зато нет индексной адресации, и шаг указателя приходится выписывать явно — тоже четыре команды. Число архитектурных команд у x86 и RV64 совпало при противоположных философиях: каждый набор экономит там, где решил экономить, и платит в другом месте.

Почему «меньше команд» — плохая метрика

Формула, к которой всё сводится, известна и скучна: время = (число команд) × (тактов на команду) × (длительность такта). RISC-аргумент 1980 года был про то, что второй множитель можно уменьшить сильнее, чем вырастет первый, и это оказалось верно. Но сегодня ни один из трёх множителей не определяется системой команд напрямую: число внутренних операций отличается от числа команд в обе стороны (разбиение и слияние), такты на операцию определяются кэшами и предсказателем переходов, а частота — техпроцессом и бюджетом мощности (Мощность и пределы). Сравним на условном пятистадийном конвейере «одну плотную команду x86» и «две команды RISC-V», делающие одно и то же.

Диаграмма показывает главное: латентность обращения в кэш данных одинакова в обоих случаях, и именно она определяет, когда станет доступен результат. Разница между «одной командой» и «двумя» — это разница в бухгалтерии фронтенда, а не в физике доступа к памяти. Порядок величины для настольных и серверных ядер примерно 2015–2025 годов: попадание в кэш первого уровня — единицы тактов, во второй — порядка полутора-двух десятков, в третий — несколько десятков, поход в DRAM — сотни тактов. Конкретные числа различаются между поколениями и моделями в разы, поэтому запоминать надо не значения, а соотношения; подробно — в главе про подсистему памяти (Подсистема памяти) и в треке про производительность (Кэш и локальность).

Как RISC-наборы забирали идеи обратно

Смешение шло с обеих сторон, и это видно по тому, что добавляли в «чистые» RISC-наборы.

  • Составные арифметические операции. Умножение со сложением одной командой — не только экономия команд, но и другая точность: промежуточный результат не округляется. Классический CISC-аргумент «одна команда делает больше полезной работы» здесь оказался прав.
  • Загрузка и сохранение пары регистров в AArch64: одна команда трогает два регистра и память. Ортодоксальный RISC этого бы не одобрил, но пролог и эпилог функции короче.
  • Криптографические и специализированные команды. Раунд AES одной командой — ровно «сложная команда под конкретную задачу», просто задача достаточно распространённая, чтобы окупить площадь. Та же логика ведёт к SIMD и дальше к ускорителям (SIMD и векторные расширения, Ускорители).
  • Автоинкрементная адресация у ARM — прямое наследие идей, за которые критиковали VAX.

AArch64: что ARM выбросил и почему

Обратное движение поучительнее. Переходя от 32-битного ARM к AArch64 (ARMv8, объявлен в 2011), ARM убрал две фирменные возможности. Предикация на каждой команде: в ARM32 почти любая команда могла исполняться условно (addeq, movne) — на коротком неглубоком конвейере отличная замена переходу, на внеочередном ядре беда, потому что команда зависит от флагов, а её приёмный регистр формально требует ещё и старого значения на случай невыполнения условия, то есть при переименовании возникает лишняя зависимость. В AArch64 остались условная выборка и несколько родственных команд. Множественная загрузка и сохранение (ldm/stm, до 16 регистров одной командой): прекрасно для плотности кода, тяжело для точных исключений — если страничный отказ случился на девятом регистре из шестнадцати, надо уметь корректно возобновиться. Это случай, когда микроархитектурная реальность продиктовала изменения в самом контракте.

Где система команд действительно решает

Если различие «RISC против CISC» почти растворилось, значит ли это, что ISA не важна? Нет. Просто важны другие её части, чем те, о которых спорили в 1980-х.

Модель памяти

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

Следствие жёсткое: код с самодельной синхронизацией через volatile и «мы же проверили, работает» переносится с x86 на ARM и ломается — не «медленнее работает», а даёт неверные результаты под нагрузкой. Единственный переносимый путь — модель памяти языка (атомики C11/C++11, sync/atomic в Go, std::sync::atomic в Rust) и её порядки (acquire/release/seq_cst); подробный разбор — в главе про многоядерность (Многоядерность) и в главе про конкурентность Rust (Конкурентность). Насколько это дорого, видно по одному решению: чтобы быстро исполнять транслированные x86-программы, процессорам Apple добавили аппаратный режим с более сильным упорядочением памяти. За разницу в модели памяти пришлось заплатить транзисторами — хороший индикатор того, что именно эта часть контракта значима.

Атомарные операции

Здесь тоже расхождение по модели, а не по «сложности»: x86 даёт готовые атомарные операции «чтение-изменение-запись» с префиксом блокировки, включая сравнение с обменом; ARMv8 изначально предлагал пару «эксклюзивная загрузка / эксклюзивное сохранение», а однокомандные атомарные операции появились в ARMv8.1; RISC-V в расширении A даёт и то и другое — lr/sc и набор amo*.

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

Регистр флагов и неявные зависимости

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

Цена отказа честная и её стоит знать: без флага переноса длинная арифметика (сложение 128-битных чисел, разбор переполнения) требует нескольких дополнительных команд, и это один из самых обоснованных пунктов критики RISC-V. Разработчики набора отвечают, что предпочитают платить в редком коде, чем усложнять переименование в каждом ядре — типичный архитектурный размен: сдвинуть сложность туда, где она встречается реже.

Наследие, от которого нельзя отказаться

Контракт ISA — это ещё и обязательства перед прошлым. x86 обязан поддерживать режимы, существующие с 1978 года, корректно исполнять самомодифицирующийся код (аппаратно отслеживая запись в область, откуда уже выбраны команды), поддерживать невыровненные обращения к памяти. Каждое обязательство — транзисторы и проверки в каждом новом ядре. Заметьте: это не «CISC-проблема», а «проблема долгой совместимости». ARM32 тоже накопил наследие и решил вопрос радикально — новым набором; у молодого RISC-V уже есть свои обязательства между профилями и версиями расширений.

Транзисторный бюджет: почему аргумент устарел, а потом вернулся

Аргумент 1980 года «микрокод и сложный декодер съедают кристалл» опирался на цифры своего времени: RISC-I — порядка сорока тысяч транзисторов, стэнфордский MIPS — ещё меньше, Intel 8086 (1978) — около двадцати девяти тысяч. При таких бюджетах декодер был заметной долей кристалла, и отдать её под регистры было очевидным выигрышем. Сегодня большой процессорный кристалл — это десятки миллиардов транзисторов, разница в шесть порядков: декодер, даже сложный, занимает ничтожную долю площади, а подавляющая часть уходит на кэши, векторные блоки и структуры внеочередного исполнения. Исходный аргумент просто перестал быть значимым — площадь больше не дефицит.

Но он вернулся в энергетической форме. Когда рост частоты остановился, а бюджет мощности стал главным ограничением, значение приобрела не площадь блока, а энергия на выполненную команду. Декодирование переменной длины — работа, которую надо делать каждый раз при промахе мимо кэша микроопераций; на больших ядрах её удаётся прятать, при попытке сделать ядро очень широким она начинает мешать. Отсюда и кэши микроопераций, и буферы циклов: вся эта инфраструктура существует ровно затем, чтобы не платить за декодирование дважды. Почему энергия стала главным ограничением — тема отдельной главы (Мощность и пределы), а почему транзисторы перестали дешеветь прежними темпами — в главе о производстве (Производство).

Что говорят прямые измерения

Самая честная попытка оценить, сколько стоит сама система команд, — работа Blem, Menon и Sankaralingam «Power Struggles: Revisiting the RISC vs. CISC Debate on Contemporary ARM and x86 Architectures» (HPCA 2013, PDF): авторы сравнивали реальные чипы разных классов на одинаковых задачах и с одинаковыми компиляторами. Вывод, огрублённо: различия в производительности и энергопотреблении объясняются микроархитектурой, техпроцессом и целевой точкой проектирования (мобильная, настольная, серверная), а не тем, RISC перед нами или CISC.

Поэтому фразы вида «эта архитектура экономичнее, потому что RISC» стоит воспринимать как маркетинг: экономичным ядро делает то, на какую точку по мощности его проектировали. Два ядра одной и той же ISA, рассчитанные на 5 ватт и на 100 ватт, отличаются между собой сильнее, чем два ядра разных ISA, рассчитанные на одинаковый бюджет.

Что из этого следует практику

Целевой уровень ISA — не «архитектура», а конкретный набор расширений. Для x86-64 существуют оговорённые уровни (от базового до уровня с широкими векторными расширениями), для ARM — версии архитектуры и профили, для RISC-V — строка вида rv64gc, буквально перечисляющая базовый набор и расширения. Сборка «под самое новое» даст бинарник, который упадёт с недопустимой командой на машине постарше; «под самое старое» — лишит код векторных возможностей. Стандартное решение — минимальный базовый уровень плюс диспетчеризация по возможностям процессора в рантайме для горячих функций.

Невыровненные обращения к памяти. На больших x86- и ARM-ядрах невыровненное чтение обычно работает и стоит недорого. На части встраиваемых ядер оно либо запрещено, либо эмулируется обработчиком исключения — медленнее в десятки-сотни раз, а не на проценты. Код, разбирающий бинарные форматы «указателем в буфер», прекрасно живёт на ноутбуке и превращается в тыкву на устройстве.

Не переносите микрооптимизации между системами команд. «Избегать лишних промахов кэша», «уменьшить число зависимостей по данным» — переносимо. «Использовать конкретную инструкцию», «рассчитывать, что сравнение и переход склеятся», «полагаться на определённую латентность» — не переносимо, и проверяется только измерением (Профилирование CPU).

Чем смотреть на реальность. Compiler Explorer — во что превращается ваш код на разных ISA и компиляторах; uops.info — измеренные характеристики команд x86-ядер; llvm-mca — статическая оценка пропускной способности участка кода; руководства Agner Fog — подробности о фронтенде. Как компилятор выбирает команды, разбирается в треке про компиляторы (Генерация кода).

Типичные заблуждения

  • «RISC быстрее CISC». Сравниваются несравнимые вещи: быстрее бывает конкретное ядро на конкретной задаче при конкретном бюджете мощности.
  • «У RISC меньше команд». Сегодня нет: полный современный набор со всеми ратифицированными расширениями содержит тысячи кодировок. «Сокращённый» означало «упрощённый до исполнимого за такт», а не «маленький».
  • «Внутри x86 — RISC-процессор». Полезное упрощение, но внутренние операции не система команд: они недокументированы, нестабильны между поколениями и не обязаны быть простыми.
  • «Меньше команд в цикле — быстрее цикл». Считать надо внутренние операции, зависимости по данным и промахи: одна команда x86 и две RISC-V выше дали одинаковую латентность, потому что обе упираются в кэш.
  • «ARM экономичнее из-за системы команд». Измерения показывают обратное: решает точка проектирования и техпроцесс.
  • «Микрокод устарел». Он никуда не делся: редкие и сложные команды по-прежнему разворачиваются микропрограммой, а обновляемый микрокод — штатный механизм исправления аппаратных ошибок в уже проданных процессорах.
  • «RISC-V — просто открытый MIPS». Разница в модели: модульные расширения, отсутствие слотов задержки и флагов, явная ориентация на слияние команд. Об этом — следующая глава.

Мини-итог

  • Спор родился из конкретных ограничений: дорогая память, слабые компиляторы, микрокод как дешёвый способ добавлять команды. Каждое из них либо исчезло, либо перевернулось.
  • RISC был не про «мало команд», а про пакет решений, делающих конвейеризацию возможной: фиксированная длина, load/store, простая адресация, много регистров, жёсткая логика.
  • x86 сохранил внешний контракт и перешёл на внутренние операции; ARM и RISC-V добавили себе плотные составные команды. Стороны сошлись примерно посередине.
  • Реальная цена переменной длины сегодня — стоимость фронтенда и энергия на декодирование, а не площадь кристалла. Отсюда кэши микроопераций и буферы циклов.
  • Значимые различия ISA лежат в другом месте: модель памяти, атомарные операции, флаги, обязательства перед совместимостью, наличие нужных расширений.
  • Прямые измерения не подтверждают, что система команд определяет производительность или энергоэффективность: решают микроархитектура, техпроцесс и целевая точка проектирования.

Источники

  • D. Patterson, D. Ditzel. The Case for the Reduced Instruction Set Computer. ACM SIGARCH Computer Architecture News, 1980 — dl.acm.org.
  • R. Colwell et al. Instruction Sets and Beyond: Computers, Complexity, and Controversy. IEEE Computer, 1985 — развёрнутое возражение сторонникам RISC, полезно читать вместе с предыдущей.
  • J. Emer, D. Clark. A Characterization of Processor Performance in the VAX-11/780. ISCA, 1984 — измерения, из которых видно, во что обходится выразительная система команд.
  • E. Blem, J. Menon, K. Sankaralingam. Power Struggles: Revisiting the RISC vs. CISC Debate on Contemporary ARM and x86 Architectures. HPCA, 2013 — PDF.
  • J. Hennessy, D. Patterson. Computer Architecture: A Quantitative Approach — базовый учебник, приложения по истории систем команд особенно уместны к этой главе.
  • Intel Software Developer Manuals — intel.com.
  • Arm Architecture Reference Manual — developer.arm.com.
  • RISC-V Technical Specifications — riscv.org.
  • J. Preshing. Weak vs. Strong Memory Models — preshing.com.

Что дальше

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

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

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

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

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

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