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) — ещё меньше транзисторов и ещё радикальнее ставка на компилятор. Общий вывод состоял из трёх наблюдений.
- Компиляторы не пользуются богатством ISA. Реальные программы состоят из загрузок, сохранений, сложений, сравнений и переходов. Чтобы применить экзотическую команду, компилятор должен распознать в исходнике ровно тот паттерн, под который она заточена, а такого совпадения почти не бывает.
- Сложная команда часто медленнее эквивалентной последовательности простых. Микропрограмма обязана обрабатывать общий случай — все режимы адресации, все проверки. Компилятор знает частный случай и выпускает три простые команды, которые исполняются быстрее одной сложной.
- Микрокод занимает площадь, которую можно потратить лучше. В машине конца 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, рассчитанные на одинаковый бюджет.
Что из этого следует практику
без блокировок?} B -->|да| C[Да: модель памяти различается.
Пользуйтесь атомиками языка,
тестируйте на слабой модели] B -->|нет| D{Пишу под
микроконтроллер?} D -->|да| E[Да: плотность кода и наличие
расширений решают.
Смотрите на сжатые кодировки] D -->|нет| F{Читаю дизассемблер
или пишу интринсики?} F -->|да| G[Да: формы команд и векторные
расширения различаются.
Переносите намерение, не приёмы] F -->|нет| H{Собираю бинарники
под несколько платформ?} H -->|да| I[Да: базовые уровни ISA и флаги
сборки различаются.
Фиксируйте минимальный уровень] H -->|нет| J[Скорее нет: работайте
с алгоритмами, локальностью
и профилем нагрузки]
Целевой уровень 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.
Что дальше
Мы несколько раз упирались в вопрос: если система команд — это в первую очередь контракт, то кто и на каких условиях им владеет? Все три набора из этой главы принадлежат компаниям, и условия у всех разные. Следующая глава разбирает набор, который устроен принципиально иначе: открытая спецификация, минимальное ядро и модульные расширения вместо монолита — что это даёт, чего не даёт и где такой подход уже работает.